最近做了几道动态规划题,发现了其中一些规律,认真复盘一下。
先来看几道题。
【1 机器人走方格】有一个XxY的网格,一个机器人只能走格点且只能向右或向下走,要从左上角走到右下角。请设计一个算法,计算机器人有多少种走法。注意网格中有些障碍点是不能走的。给定一个int[][] map(C++ 中为vector >),表示网格图,若map[i][j]为1则说明该点不是障碍点,否则则为障碍。另外给定int x,int y,表示网格的大小。请返回机器人从(0,0)走到(x - 1,y - 1)的走法数。
【2 洪水】在一个nxm矩阵形状的城市里爆发了洪水,洪水从(0,0)的格子流到这个城市,在这个矩阵中有的格子有一些建筑,洪水只能在没有建筑的格子流动。请返回洪水流到(n - 1,m - 1)的最早时间,(洪水只能从一个格子流到其相邻的格子且洪水单位时间能从一个格子流到相邻格子)。
【3 硬币表示】有数量不限的硬币,币值为25分、10分、5分和1分,请编写代码计算n分有几种表示法。给定一个int n,请返回n分有几种表示法。
以上几道题都有一个共同点就是有很多种可能,并且每一步都取决于上一步的结果,整个过程是动态的。
这个过程其实和递归有点像,但是过程还是不一样的。
动态规划就是:将原问题拆解成若干子问题,同时保存子问题的答案,使得每个子问题只求解一次,最终获得原问题的答案。
递归是:要解决原问题,先解决比原问题规模更小的子问题。解决了小问题,再回过头来获得最终原问题的答案。
动态规划是显式得把每一步的结果保存下来,而递归则是把每一子步的结果存在内存中,一调用完成就回销毁,因此同一个子问题可能会被求解多次。
因此动态规划本质就是用空间换时间。
分为以下三步:
1 找到初始解。
2 找到规律,对于任意的i,如何通过之前的解推断出来?
3 找到遍历路径
4 循环
先看第一题,【机器人走方格】
我们用一个dp数组用来保存走到每一个格子的走法数,题中要求的第(x-1, y-1)格子的走法数就是dp[x-1][y-1]。
1 找到初始解
dp[0][0]=1,因为只有一种解法。
2 找到规律。
由于题中规定,只能从左往右或从上往下,对于任意的i和j,要走到dp[i][j],只能从dp[i-1][j] 或 dp[i][j-1]走过去。因此dp[i][j] = dp[i-1][j] + dp[i][j-1]。
当i==0或j==0时, dp[0][j] = dp[0][j-1], dp[i][0] = dp[i-1][0]。
另外,当map[i][j]不等于1时,表示此格不通。此时保留dp[i][j]=0即可,表示没有方法可以到达。
3 找到路径
为了得到最后一个格子数,逐行遍历即可。
4 循环
int countWays(vector<vector<int> > map, int x, int y) { vector<vector<int> > dp(x, vector<int>(y, 0)); for(int i=0;i<x;i++) { for(int j=0;j<y;j++) { if(map[i][j]!=1) continue; if(i==0 && j== 0) dp[0][0] = 1; else if(i==0) dp[0][j] = dp[0][j-1]; else if(j==0) dp[i][0] = dp[i-1][0]; else dp[i][j] = (dp[i-1][j] + dp[i][j-1])%1000000007; } } return dp[x-1][y-1]; }
再看第二题,【洪水】
同样用数组dp[i][j]用来表示到达该格子的最短时间。
1 找到初始解
洪水从第一个格子开始,因此dp[0][0] = 0。
2 找到规律
对于任意格子ij来说,洪水可以从四个方向过来。第一反应就是周围四个格子的值取最小即可,即dp[i][j] = min(dp[i-1][j], dp[i+1][j], dp[i][j-1], dp[i][j+1]) + 1。 但是真正运行起来可以发现,周围四个格子值其实也取决于ij这个格子的值本身,也就是说这五个格子之间的值是耦合在一起的。因此,不能直接用上述最小的式子得到。
于是可以换种思路,虽然无法从周围的格子得到ij的,但是假如已知dp[i][j]的话,其余四个格子的值都可以得到,就是dp[i][j] + 1。这样不断更新每个格子的值,取最小的即可。
3 遍历路径
由于洪水可以往四个方向流动,因此不能像之前那样逐行遍历,因为在遍历过程中后面的值极有可能改变前面的值。因此这里借助一个队列来遍历全部的格子。
每遍历一个格子,就把上下左右四个格子分别加入到队列中。事先给定的map中格子的值为1表示有建筑,假如格子的值为0表示没建筑。假如遍历过,那么格子的值就不再是0。因此可以通过dp[i][j]来判断是否要将它加入队列。
4 循环
int floodFill(vector<vector<int> > map, int n, int m) { queue<int> que; if(n<0 || m<0 || map[0][0]==1) return 0; int direction[4][2] = {{1,0},{-1, 0},{0, 1},{0,-1}}; que.push(0); while(!que.empty()) { int current = que.front(); que.pop(); int row = current/m; //当前格子的行数 int col = current%m; //当前格子的列数 if(row == n-1 && col == m-1) return map[row][col]; for(int i=0;i<4;i++) { int new_row = row + direction[i][0]; //下一个格子的行数 int new_col = col + direction[i][1]; //下一个格子的列数 if(new_row>=0 && new_row<n && new_col >=0 && new_col<m && map[new_row][new_col]==0) { map[new_row][new_col] = map[row][col] + 1; que.push(new_row * m + new_col); } } } return 0; }
再看最后一题,【硬币表示】
1 找到初始解
当 n=1,那么只有1种解法,只能用1表示。
2 找到规律
对于n来说,对于coin = 5,dp[n] =dp[n] + dp[n-5], 同理,对于coin =10, dp[n] = dp[n] + dp[n-10] 。
3 遍历路径
这里其实存在有两个维度来遍历,一个是不断增长的n,另一个是币种数。
假如只有一种币种,例如1,那么可以求解每一个n的组合方式(都是1)。每增加一种币种,就多了一种新的表示方式,把每一种币种表示方式加起来其实就是最终的结果。
4 循环
int countWays(int n) { int coins[4] = {1, 5, 10, 25}; int dp[100001] = {0}; dp[0] = 1; for(int i=0;i<4;i++) { for(int j = coins[i];j<=n;j++) dp[j] = (dp[j] + dp[j-coins[i]]); } return dp[n]; }
暂时总结到这里,以后还有新的补充再写。