我有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);
}
但它不是最优的(例如在上面的例子中它给出了100)
是否有更好的最佳子结构的想法?我能用动态编程来解决它吗?!
答案 0 :(得分:1)
让我们重新考虑您的任务的Bellman最优方程。我认为这是解决此类问题的系统方法(而我常常不了解DP单行)。我的参考是book of Sutton and Barto。
系统所在的状态可以用三个整数(i,j,r)
(建模为std::array<int,3>
)来描述。此处,i
和j
表示矩形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;
}
的零数。
这是一个重要的事情:在制作中,你通常不会像我一样调用1
,因为它需要指数级增加的递归调用。相反,你从左上角开始调用它,然后存储结果。接下来,您左右移动,每次调用backwardIteration
,现在使用之前存储的结果。等等。
为了做到这一点,需要在函数backwardIteration
中使用memoization概念,该概念返回已存储的结果,而不是调用另一个递归调用。
我在上面的函数调用中添加了memoization。现在,您可以以任何您喜欢的方式从左上方到右下方循环遍历数组 - 但优选采用小步骤,例如逐行,逐列或矩形矩形。
事实上,这只是动态编程的精神。