将所有路径上的数字相乘,得到一个具有最小零数的数字

时间:2014-11-21 10:24:46

标签: c++ algorithm dynamic-programming reinforcement-learning

我有m*n表,每个条目都有一个值。

开始位置位于左上角角落,我可以向右向下,直到我到达右下方角。

我想要一条路径,如果我在该路径上乘以数字,我会得到一个在其右侧具有最小零数的数字。

示例:

   1 2 100
   5 5 4

可能的路径:

1*2*100*4=800
1*2*5*4= 40
1*5*5*4= 100

解决方案:1*2*5*4= 40因为40有1个零但其他路径有2个零。

最简单的方法是使用dfs并计算所有路径。但效率不高。

我正在寻找一个最佳的子结构来使用动态编程解决它。

思考了一会后,我想到了这个等式:

T(i,j) = CountZeros(T(i-1,j)*table[i,j]) < CountZeros(T(i,j-1)*table[i,j]) ?
                 T(i-1,j)*table[i,j] : T(i,j-1)*table[i,j]

代码:

#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>

using namespace std;
using Table = vector<vector<int>>;
const int rows = 2;
const int cols = 3;
Table memo(rows, vector<int>(cols, -1));

int CountZeros(int number)
{
    if (number < 0)
        return numeric_limits<int>::max();
    int res = 0;
    while (number != 0)
    {
        if (number % 10 == 0)
            res++;
        else break;
        number /= 10;
    }
    return res;
}


int solve(int i, int j, const Table& table)
{
    if (i < 0 || j < 0)
        return -1;

    if (memo[i][j] != -1)
        return memo[i][j];

    int up = solve(i - 1, j, table)*table[i][j];
    int left = solve(i, j - 1, table)*table[i][j];

    memo[i][j] = CountZeros(up) < CountZeros(left) ? up : left;

    return memo[i][j];
}
int main()
{
    Table table =
    {
        { 1, 2, 100 },
        { 5, 5, 4 }
    };

    memo[0][0] = table[0][0];
    cout << solve(1, 2, table);

}

(Run )

但它不是最优的(例如在上面的例子中它给出了100)

是否有更好的最佳子结构的想法?我能用动态编程来解决它吗?!

1 个答案:

答案 0 :(得分:1)

让我们重新考虑您的任务的Bellman最优方程。我认为这是解决此类问题的系统方法(而我常常不了解DP单行)。我的参考是book of Sutton and Barto

系统所在的状态可以用三个整数(i,j,r)(建模为std::array<int,3>)来描述。此处,ij表示矩形M = m_{i,j}中的列和行,而r表示乘法结果。

状态(i,j,r)中的操作是通过转发right来提供的,您以状态(i, j+1, r*m_{i,j+1})结束,或者通过下降转到州{ {1}}。

然后,Bellman方程由

给出
(i+1, j, r*m_{i+1,j})

这个等式背后的基本原理如下:v(i,j,r) = min{ NullsIn(r*m_{i+1,j}) - NullsIn(r) + v_(i+1,j, r*m_{i+1,j}) NullsIn(r*m_{i,j+1}) - NullsIn(r) + v_(i,j+1, r*m_{i,j+1}) } 表示当你采取两个动作中的一个时必须添加的零,即即时惩罚。 NullsIn(r*m_{i+1,j}) - NullsIn(r)表示您采取此操作时所处的状态中的零。现在,人们希望采取最小化这两种贡献的行动。

您还需要的只是一个函数v_(i+1,j, r*m_{i+1,j}),它返回给定整数中的空值。这是我的尝试:

int NullsIn(int)

为方便起见,我进一步定义了int NullsIn(int r) { int ret=0; for(int j=10; j<=r; j*=10) { if((r/j) * j == r) ++ret; } return ret; } 函数:

NullsDifference

现在,必须从矩阵右下角元素的初始状态开始向后迭代。

int NullsDifference(int r, int m)
{
    return NullsIn(r*m) - NullsIn(r);
}

此例程通过

调用
int backwardIteration(std::array<int,3> state, std::vector<std::vector<int> > const& m)
{
    static std::map<std::array<int,3>, int> memoization;
    auto it=memoization.find(state);
    if(it!=memoization.end())
        return it->second;

    int i=state[0];
    int j=state[1];
    int r=state[2];

    int ret=0;
    if(i>0 && j>0)
    {
        int inew=i-1;
        int jnew=j-1;

        ret=std::min(NullsDifference(r, m[inew][j]) + backwardIteration({inew,j,r*m[inew][j]}, m),
                     NullsDifference(r, m[i][jnew]) + backwardIteration({i,jnew,r*m[i][jnew]}, m));
    }
    else if(i>0)
    {
        int inew=i-1;
        ret= NullsDifference(r, m[inew][j]) + backwardIteration({inew,j,r*m[inew][j]}, m);
    }
    else if(j>0)
    {
        int jnew=j-1;
        ret= NullsDifference(r, m[i][jnew]) + backwardIteration({i,jnew,r*m[i][jnew]}, m);
    }

    memoization[state]=ret;
    return ret;
}

对于您的数组,它会打印出所需的int main() { int ncols=2; int nrows=3; std::vector<std::vector<int> > m={{1,2,100}, {5,5,4}}; std::array<int,3> initialState = {ncols-1, nrows -1, m[ncols-1][nrows - 1]}; std::cout<<"Minimum number of zeros: "backwardIteration(initialState, m)<<"\n"<<std::endl; } 的零数。

这是live demo on Coliru


修改

这是一个重要的事情:在制作中,你通常不会像我一样调用1,因为它需要指数级增加的递归调用。相反,你从左上角开始调用它,然后存储结果。接下来,您左右移动,每次调用backwardIteration,现在使用之前存储的结果。等等。

为了做到这一点,需要在函数backwardIteration中使用memoization概念,该概念返回已存储的结果,而不是调用另一个递归调用。

我在上面的函数调用中添加了memoization。现在,您可以以任何您喜欢的方式从左上方到右下方循环遍历数组 - 但优选采用小步骤,例如逐行,逐列或矩形矩形。

事实上,这只是动态编程的精神。