动态规划刷题总结(一)
动态规划基本要素
1.最优子结构:当问题的最优解包含了其子问题的最优解时,称该问题具有最优子结构性。
2.重叠子问题:每次产生的子问题不总是新问题,有些子问题被反复计算多次。
3.备忘录方法:采用备忘录方法来记录子问题的结果,当需要重复用到某个子问题的结果时,可直接在备忘录中查找。
LeetCode刷题记录
474.一零和
问题描述:
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
求解关键:一开始,我想到的是用记录某个子集的0和1的个数,但深入思考后发现这样是无法求解的,
数据结构会变得很复杂;而且这样的想法是没有认识到动态规划问题的最优子结构性。
用数组dp[j][k]
来记录当0的个数为j,1的个数为k时的最大子集数;然后将字符串数组中的元素
一个一个加入到最大子集中,如果j和k大于当前加入字符串的0和1的个数,则更新d[j][k]
:
dp[j][k]=(dp[j-zero][k-one]+1>dp[j][k])?dp[j-zero][k-one]+1:dp[j][k];
其中zero
和one
是当前字符串的0和1的个数。
求解代码:
class Solution {
public:
int findMaxForm(vector<string>& strs, int m, int n) {
int size=strs.size();
vector<vector<int>> count01;
for(int i=0;i<size;i++){
count01.push_back({0,0});
for(int j=0;j<strs[i].size();j++){
if(strs[i][j]=='0')
count01[i][0]++;
else
count01[i][1]++;
}
}
vector<vector<int>> dp(m+1,vector<int>(n+1));
for(int i=0;i<size;i++){
int zero=count01[i][0];
int one=count01[i][1];
for(int j=m;j>=count01[i][0];j--){
for(int k=n;k>=count01[i][1];k--){
dp[j][k]=(dp[j-zero][k-one]+1>dp[j][k])?dp[j-zero][k-one]+1:dp[j][k];
}
}
}
}
注意:代码中j跟k都是从大到小来循环,这是因为从小到大更新d[j][k]
时,会把更新时需要比较的
dp[j-zero][k-one]
覆盖掉,dp[j-zero][k-one]
很可能是已经加过1了的,这样就会导致结果错
误。
其次,我的解法中是已经压缩了一些状态的,如若不压缩状态,需用三维数组来储存子问题结果。
787.K站中转内最便宜的航班
问题描述:
有 n 个城市通过 m 个航班连接。每个航班都从城市 u 开始,以价格 w 抵达 v。
现在给定所有的城市和航班,以及出发城市 src 和目的地 dst,你的任务是找到从 src 到 dst 最多经过 k
站中转的最便宜的价格。 如果没有这样的路线,则输出 -1。
求解关键:
怎样去判断选择的中转节点是可以到达终点?看了题解之后思路才清晰。
其实并不需要判断中转节点能不能到达终点,只需要求一个个子问题就ok了。
用dp[i][dst]
来存储从起点经过i个中转点到达dst的最低价值。初始条件如下:
1.vector<vector<int>> dp(K+1,vector<int>(n,INT_MAX));
初始默认所有节点都是无法在i个中转
点到达的
2.for(int i=0;i<=K;i++)
dp[i][src]=0;
起点到起点本身的价值为0
3.for(auto &flight:flights){
if(flight[0]==src)
dp[0][flight[1]]=flight[2];
}
所有与起点之间有航班的城市,经过0个中转点到达所花费的时间就是与起点间航班的价值。
代码如下:
class Solution {
public:
int findCheapestPrice(int n, vector<vector<int>>& flights, int src, int dst, int K) {
vector<vector<int>> dp(K+1,vector<int>(n,INT_MAX));
for(int i=0;i<=K;i++)
dp[i][src]=0;
for(auto &flight:flights){
if(flight[0]==src)
dp[0][flight[1]]=flight[2];
}
for(int i=1;i<=K;i++){
for(auto &flight:flights){
if(dp[i-1][flight[0]]!=INT_MAX)
dp[i][flight[1]]=(dp[i][flight[1]]<dp[i-1][flight[0]]+flight[2])?dp[i][flight[1]]:dp[i-1][flight[0]]+flight[2];
}
}
return dp[K][dst]==INT_MAX?-1:dp[K][dst];
}
};
467.环绕字符串唯一的子字符串
问题描述:
把字符串 s 看作是“abcdefghijklmnopqrstuvwxyz”的无限环绕字符串,所以 s 看起来是这样
的:"…zabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcd…".
现在我们有了另一个字符串 p 。你需要的是找出 s 中有多少个唯一的 p 的非空子串,尤其是当你的输入
是字符串 p ,你需要输出字符串 s 中 p 的不同的非空子串的数目。
求解关键:
由于字符串s是a-z的无限循环,因此s中包含的p的非空子串一定是连续的(包含’az‘)或者单个的字母。
考虑备忘录中记录什么,是一个很关键的问题。如果能找到这个问题的答案,那就离解出问题不远了。
考虑到s中包含的p的非空子串的特性,可以考虑选择每一个连续非空子串的最后一个字母来记录以它为结尾的最长子串的长度。
又由观察可知
p = ‘a’, 以a结尾的连续字符长度为1,以a结尾的非空字串数目为1 (a).
p = ‘ab’,以b结尾的连续字符长度为2,以b结尾的非空字串数目为2 (b, ab).
p = ‘abc’,以c结尾的连续字符长度为3,以c结尾的非空子串数目为3 (c, bc, abc).
长度为h的连续字符串p,以p的结尾字母为结尾的非空子串数目为h。
设以当前位置字符结尾的字串长度为len,以该字符结尾的之前出现过的长度为len1.
len > len1, result += len - len1, 更新len到原有len1.
len <= len1, 以当前字符结尾的连续字串在之前都出现过,不做修改。
代码如下:
class Solution {
public:
int findSubstringInWraproundString(string p) {
int length=p.length();
if(length==0)
return 0;
vector<int> dp(26,0);
int answer=1;
int curlen=1;
dp[p[0]-'a']=1;
for(int i=1;i<length;i++){
if(p[i]==p[i-1]+1 || (p[i]=='a'&&p[i-1]=='z'))
curlen++;
else
curlen=1;
if(curlen>dp[p[i]-'a']){
answer+=curlen-dp[p[i]-'a'];
dp[p[i]-'a']=curlen;
}
}
return answer;
}
};
注意:
为什么设answer的初值为1呢?这是因为只要p的长度不为0,p的首字母必定是符合条件的非空子串,
而后面的循环中是不包含p的首字母的。同时,dp[p[0]-'a']
也要先进行更新。