记录结果再利用的"动态规划"
01背包问题
- 问题描述:有n个重量和价值分别为wi、vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
- 限制条件:
- 1≤n≤100
- 1≤wi、vi≤100
- 1≤W≤10000
- 分析:
- 不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看:
1 #include <iostream> 2 using namespace std; 3 4 int n,W; 5 int *w,*v; 6 7 int max(int x, int y) 8 { 9 if (x>y) return x; 10 return y; 11 } 12 13 int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体 14 { 15 int res; 16 if (i==n) res=0; 17 else if (j<w[i]) res=rec(i+1,j); 18 else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]); 19 return res; 20 } 21 22 int main() 23 { 24 cin >> n >> W; 25 w = new int[n]; 26 v = new int[n]; 27 for (int i=0; i<n; i++) cin >> w[i] >> v[i]; 28 cout << rec(0,W) << endl; 29 }
这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2n)的时间。
- 通过分析ren递归调用的情况,我们可以发现rec()函数对于相同的参数进行了多次调用,因此进行了很多遍相同的计算过程,如果我们把第一次计算的结果记录下来,那么就可以省掉第二次及以后的重复计算,这种方法叫做记忆话搜索,对于同样的参数,只会在第一次被调用到时执行递归部分,第二次之后都会直接返回结果,参数的组合不过nW种,而函数内只调用2次递归,所以只需要O(nW)的复杂度:
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int *w,*v; 7 int **dp; 8 9 int max(int x, int y) 10 { 11 if (x>y) return x; 12 return y; 13 } 14 15 int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体 16 { 17 if (j[i[dp]]>=0) return j[i[dp]]; 18 int res; 19 if (i==n) res=0; 20 else if (j<w[i]) res=rec(i+1,j); 21 else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]); 22 return j[i[dp]] = res; 23 } 24 25 int main() 26 { 27 cin >> n >> W; 28 w = new int[n]; 29 v = new int[n]; 30 dp = new int*[n+1]; 31 for (int i=0; i<=n; i++) 32 { 33 dp[i] = new int[W+1]; 34 memset(dp[i],-1,sizeof(int)*(W+1)); 35 } 36 for (int i=0; i<n; i++) cin >> w[i] >> v[i]; 37 cout << rec(0,W) << endl; 38 }
其中memset()函数时按照1字节为单位对内存进行填充的,通过使用memset可以快速地对高维数组等进行初始化
- 在需要剪枝的情况下,可能会把各种参数都写在函数上,但是在这种情况下会让记忆化搜索难以实现,需要注意
- 研究一下记忆化数组,记dp[i][j]为从第i个物品(编号为i的物品)开始挑选总重小于j时,总价值的最大值。于是我们就有如下的递推式:
dp[n][j]=0
/ dp[i+1][j] (j<w[i]时)
dp[i][j] =
\ max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]) (其它情况下)
如上所示,不同写递归函数,直接利用递推式将各项的值计算出来,简单地用二重循环也可以解决这一问题,复杂度为O(nW),与记忆化搜索是一样的,但是简洁了很多,这种方法叫做动态规划,即常说的DP:1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int *w,*v; 7 int **dp; 8 9 int max(int x, int y) 10 { 11 if (x>y) return x; 12 return y; 13 } 14 15 int main() 16 { 17 cin >> n >> W; 18 w = new int[n]; 19 v = new int[n]; 20 dp = new int*[n+1]; 21 for (int i=0; i<=n; i++) 22 { 23 dp[i] = new int[W+1]; 24 memset(dp[i],0,sizeof(int)*(W+1)); 25 } 26 for (int i=0; i<n; i++) cin >> w[i] >> v[i]; 27 for (int i=n-1; i>=0; i--) 28 { 29 for (int j=0; j<=W; j++) 30 { 31 if (j<w[i]) dp[i][j]=dp[i+1][j]; 32 else dp[i][j] = max(dp[i+1][j],dp[i+1][j-w[i]]+v[i]); 33 } 34 } 35 cout << dp[0][W] << endl; 36 }
- 此外,我们还有各种各样的DP方式:
1.刚刚的DP中关于i的循环是逆向进行的,那如果按照如下的方式定义递推关系的话,关于i的循环就可以正向进行:
dp[i+1][j] := 从前i+1个物品(即从编号为0到i这i+1个物品)中选出总重量不超过j的物品时总价值的最大值
dp[0][j] = 0
/ dp[i][j] (j<w[i]时)
dp[i+1][j] =
\ max(dp[i][j],dp[i][j-w[i]]+v[i]) (其它情况下)
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int *w,*v; 7 int **dp; 8 9 int max(int x, int y) 10 { 11 if (x>y) return x; 12 return y; 13 } 14 15 int main() 16 { 17 cin >> n >> W; 18 w = new int[n]; 19 v = new int[n]; 20 dp = new int*[n+1]; 21 for (int i=0; i<=n; i++) 22 { 23 dp[i] = new int[W+1]; 24 memset(dp[i],0,sizeof(int)*(W+1)); 25 } 26 for (int i=0; i<n; i++) cin >> w[i] >> v[i]; 27 for (int i=0; i<n; i++) 28 { 29 for (int j=0; j<=W; j++) 30 { 31 if (j<w[i]) dp[i+1][j]=dp[i][j]; 32 else dp[i+1][j] = max(dp[i][j],dp[i][j-w[i]]+v[i]); 33 } 34 } 35 cout << dp[n][W] << endl; 36 }
2.除了运用递推方式逐项求解之外,还可以把状态转移想象成从"前i个物品中选取总重量不超过j时的状态"向"前i+1个物品中选取总重量不超过j"和"从前i+1个物品中选取总重量不超过j+w[i]时的状态"的转移:
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int *w,*v; 7 int **dp; 8 9 int max(int x, int y) 10 { 11 if (x>y) return x; 12 return y; 13 } 14 15 int main() 16 { 17 cin >> n >> W; 18 w = new int[n]; 19 v = new int[n]; 20 dp = new int*[n+1]; 21 for (int i=0; i<=n; i++) 22 { 23 dp[i] = new int[W+1]; 24 memset(dp[i],0,sizeof(int)*(W+1)); 25 } 26 for (int i=0; i<n; i++) cin >> w[i] >> v[i]; 27 for (int i=0; i<n; i++) 28 { 29 for (int j=0; j<=W; j++) 30 { 31 dp[i+1][j] = max(dp[i+1][j],dp[i][j]); 32 if (j+w[i]<=W) dp[i+1][j+w[i]] = max(dp[i+1][j+w[i]],dp[i][j]+v[i]); 33 } 34 } 35 cout << dp[n][W] << endl; 36 }
如果像上面这样,把问题写成从当前状态转移成下一状态的形式的话,需要特别注意初项之外也需要初始化,在这个问题中,因为价值总和至少是0,所以初值设为0就可以了,不过根据问题也有可能需要初始化成无穷大。
- 不妨先用最朴素的方法,针对每个物品是否放入背包进行搜索试试看:
最长公共子序列问题
- 问题描述:给定两个字符串s1s2…sn和t1t2…tn。求这两个字符串最长的公共子序列的长度。
- 限制条件:1≤n,m≤1000
- 分析:这个问题是被称为最长公共子序列问题(LCS,Longest Common Subsequence)的著名问题。不妨使用下面的定义:
dp[i][j] :=s1…si和t1…tj对应的LCS的长度
由此,s1…si+1和t1…tj+1对应的公共子列可能是
①当si+1=tj+1时,在s1…si和t1…tj的LCS末尾追加上si+1;
②s1…si和t1…tj+1的LCS;
③s1…si+1和t1…tj和LCS;
三者中的某一个,所以就有如下的递推关系成立:
/ max(dp[i][j]+1,dp[i][j+1],dp[i+1][j]) (si+1=tj+1)
dp[i+1][j+1] =
\ max(dp[i][j+1],dp[i+1][j]) (其它情况下)
然而,稍微思考一下,就能发现当si+1=tj+1时,只需令dp[i+1][j+1]=dp[i][j]+1就可以了
于是,总的递推式可写为:
/ dp[i][j]+1 (si+1=tj+1)
dp[i+1][j+1] =
\ max(dp[i][j+1],dp[i+1][j]) (其它情况下)
复杂度为O(nm),dp[n][m]就是LCS的长度 - 代码:
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,m; 6 char * s; 7 char * t; 8 int **dp; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> m; 19 s = new char[n+1]; 20 t = new char[m+1]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> s[i]; 24 } 25 for (int i=0; i<m; i++) 26 { 27 cin >> t[i]; 28 } 29 dp = new int*[n+1]; 30 for (int i=0; i<=n; i++) 31 { 32 dp[i] = new int[m+1]; 33 memset(dp[i],0,sizeof(int)*(m+1)); 34 } 35 for (int i=0; i<n; i++) 36 { 37 for (int j=0; j<m; j++) 38 { 39 if (s[i]==t[j]) dp[i+1][j+1]=dp[i][j]+1; 40 else dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]); 41 } 42 } 43 cout << dp[n][m] << endl; 44 }
完全背包问题
- 问题描述:有n种重量和价值分别为wi,vi的物品,从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值,在这里,每种物品可以挑选任意多件。
- 限制条件:
- 1≤n≤100
- 1≤wi,vi≤100
- 1≤W≤10000
- 分析:
- 这次同一种类的物品可以选择任意多件了,尝试着写出递推关系:
dp[i+1][j] := 从前i+1种(编号)物品中挑选总重量不超过j时总价值的最大值.
dp[0][j]=0
dp[i+1][j]=max{dp[i][j-k*w[i]]+k*v[i]|k≥0}
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int * w; 7 int * v; 8 int **dp; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> W; 19 w = new int[n]; 20 v = new int[n]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> w[i] >>v[i]; 24 } 25 dp = new int*[n+1]; 26 for (int i=0; i<=n; i++) 27 { 28 dp[i] = new int[W+1]; 29 memset(dp[i],0,sizeof(int)*(W+1)); 30 } 31 for (int i=0; i<n; i++) 32 { 33 for (int j=0; j<=W; j++) 34 { 35 for (int k=0; k*w[i]<=j; k++) 36 { 37 dp[i+1][j] = max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]); 38 } 39 } 40 } 41 cout << dp[n][W] << endl; 42 }
- 上面的程序是三重循环的,关于k的循环最坏可能从0到W,所以这个算法的复杂度为O(nW2),这样并不够好
我们来找一找这个算法中多余的计算(已经知道结果的计算),在dp[i+1][j]的计算中选择k(k≥1)个的情况,与在dp[i+1][j-w[i]]的计算中选择k-1个情况是相同的,所以dp[i+1][j]的递推中k≥1部分的计算已经在dp[i+1][j-w[i]]的计算中完成了:
dp[i+1][j]
= max{dp[i][j-k*w[i]]+k*v[i]|k≥0}
= max(dp[i][j],max{dp[i][j-k*w[i]]+k*v[i]|k≥1})
= max(dp[i][j],max{dp[i][(j-w[i])-k*w[i]]+k*v[i]|k≥0}+v[i])
= max(dp[i][j],dp[i+1][j-w[i]]+v[i])
即:dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i])
这样处理之后,就不需要关于k的循环了,现在的复杂度为O(nW):1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int * w; 7 int * v; 8 int **dp; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> W; 19 w = new int[n]; 20 v = new int[n]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> w[i] >>v[i]; 24 } 25 dp = new int*[n+1]; 26 for (int i=0; i<=n; i++) 27 { 28 dp[i] = new int[W+1]; 29 memset(dp[i],0,sizeof(int)*(W+1)); 30 } 31 for (int i=0; i<n; i++) 32 { 33 for (int j=0; j<=W; j++) 34 { 35 if (j<w[i]) dp[i+1][j] = dp[i][j]; 36 else dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i]); 37 } 38 } 39 cout << dp[n][W] << endl; 40 }
- 此外,此前提到的01背包问题和这里的完全背包问题,可以利用一维数组来实现:
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int * w; 7 int * v; 8 int *dp; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> W; 19 w = new int[n]; 20 v = new int[n]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> w[i] >>v[i]; 24 } 25 dp = new int[W+1]; 26 memset(dp,0,sizeof(int)*(W+1)); 27 for (int i=0; i<n; i++) 28 { 29 for (int j=W; j>=w[i]; j--) 30 { 31 dp[j] = max(dp[j],dp[j-w[i]]+v[i]); 32 } 33 } 34 cout << dp[W] << endl; 35 }
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int * w; 7 int * v; 8 int *dp; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> W; 19 w = new int[n]; 20 v = new int[n]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> w[i] >>v[i]; 24 } 25 dp = new int[W+1]; 26 memset(dp,0,sizeof(int)*(W+1)); 27 for (int i=0; i<n; i++) 28 { 29 for (int j=w[i]; j<=W; j++) 30 { 31 dp[j] = max(dp[j],dp[j-w[i]]+v[i]); 32 } 33 } 34 cout << dp[W] << endl; 35 }
可以发现,两者只有关于j的循环方向不同,仔细想来,是非常有道理的
- 除了上面的情况外,还有可能通过将两个数组滚动使用来实现重复利用,例如之前的
dp[i+1][j] = max(dp[i][j], dp[i+1][j-w[i]]+v[i])
这一递推式中,dp[i+1]计算时只需要dp[i]和dp[i+1],所以可以结合奇偶性写成:
1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 int n,W; 6 int * w; 7 int * v; 8 int *dp[2]; 9 10 int max(int x, int y) 11 { 12 if (x>y) return x; 13 return y; 14 } 15 16 int main() 17 { 18 cin >> n >> W; 19 w = new int[n]; 20 v = new int[n]; 21 for (int i=0; i<n; i++) 22 { 23 cin >> w[i] >>v[i]; 24 } 25 dp[0] = new int[W+1]; 26 dp[1] = new int[W+1]; 27 memset(dp[0],0,sizeof(int)*(W+1)); 28 memset(dp[1],0,sizeof(int)*(W+1)); 29 for (int i=0; i<n; i++) 30 { 31 for (int j=0; j<=W; j++) 32 { 33 if (j<w[i]) dp[(i+1) & 1][j] = dp[i & 1][j]; 34 else dp[(i+1) & 1][j] = max(dp[i & 1][j],dp[i & 1][j-w[i]]+v[i]); 35 } 36 } 37 cout << dp[n & 1][W] << endl; 38 }
- 这次同一种类的物品可以选择任意多件了,尝试着写出递推关系:
01背包问题之2
- 问题描述:有n个重量和价值分别为wi、vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
- 限制条件:
- 1≤n≤100
- 1≤wi≤107
- 1≤vi≤100
- 1≤W≤109
- 这一问题与最初的01背包问题相比,只是修改了限制条件的大小。此前求解此问题的方法的复杂度是O(nW),对于这一问题的规模来讲就不够用了。在这个问题中,相比较重量而言,价值的范围比较小,所以可以试着改变DP的对象。之前的方法中,我们用DP针对不同的重量限制计算最大的价值,在这里,我们不妨尝试着用DP针对不同的价值计算最小的重量:
dp[i+1][j] := 从前i+1个物品(编号从0到i)中挑选出价值总和为j时总重量的最小值(不存在时就是一个充分大的数值INF)
由于前0个物品中什么都挑选不了,所以初始值为
dp[0][0] = 0
dp[0][j] = INF
此外,从前i个物品中挑选出价值总和为j时,一定有
①前i-1个物品中挑选价值总和为j的部分
②前i-1个物品中挑选价值总和为j-v[i]的部分,然后再选中第i个物品
这两种方法之一,所以就得到递推式:
dp[i+1][j] = min(dp[i][j], dp[i][j-v[i]]+w[i])
最终的答案就对应于令dp[n][j]≤W的最大的j,这样求解的时间复杂度为O(n∑vi),对此题限制条件下的输入就可以在时间限制内解出了。当然如果价值变大了的话,这里的算法也变得不可行了,我们需要依据问题的规模来改变算法1 #include <iostream> 2 #include <cstring> 3 using namespace std; 4 5 const int INF = 0x3FFFFFF; 6 int n,W; 7 int * w; 8 int * v; 9 int **dp; 10 11 int min(int x, int y) 12 { 13 if (x>y) return y; 14 return x; 15 } 16 17 int main() 18 { 19 cin >> n >> W; 20 w = new int[n]; 21 v = new int[n]; 22 int maxv=0; 23 for (int i=0; i<n; i++) 24 { 25 cin >> w[i] >>v[i]; 26 if (v[i]>maxv) maxv=v[i]; 27 } 28 dp= new int*[n+1]; 29 for (int i=0; i<=n; i++) 30 { 31 dp[i] = new int[n*maxv+1]; 32 for (int j=0; j<=n*maxv; j++) dp[i][j]=INF; 33 } 34 dp[0][0] = 0; 35 for (int i=0; i<n; i++) 36 { 37 for (int j=0; j<=n*maxv; j++) 38 { 39 if (j<v[i]) dp[i+1][j] = dp[i][j]; 40 else dp[i+1][j] = min(dp[i][j],dp[i][j-v[i]]+w[i]); 41 } 42 } 43 for (int i=n*maxv; i>=0; i--) 44 if (dp[n][i] <= W) 45 { 46 cout << i << endl; 47 break; 48 } 49 }
多重部分和问题
- 问题描述:有n种不同大小的数字ai,每种各mi个,判断是否可以从这些数字之中选出若干使它们的和恰好为K。
- 限制条件:
- 1≤n≤100
- 1≤ai,mi≤100000
- 1≤K≤100000
- 分析:
- 这个问题可以用DP来做,不过如何定义递推关系会影响到最终的复杂度。先看这个定义:
dp[i+1][j] := 用前i种数字是否能加和成j
为了用前i种数字加和成j,也就需要能用前i-1种数字加和成j,j-ai,…,j-mi*ai中的某一种,由此可以得出如下递推关系:
dp[i+1][j] = (0≤k≤mi且k*ai≤j时存在使dp[i][j-k*ai]为真的k)
这个算法的时间复杂度是O(K*∑mi)1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 int n,K; 7 int * a; 8 int * m; 9 bool ** dp; 10 11 int main() 12 { 13 cin >> n >> K; 14 a = new int[n]; 15 m = new int[n]; 16 for (int i=0; i<n; i++) 17 cin >> a[i] >> m[i]; 18 dp = new bool*[n+1]; 19 for (int i=0; i<=n; i++) 20 { 21 dp[i] = new bool[K+1]; 22 memset(dp[i],false,sizeof(bool)*(K+1)); 23 } 24 dp[0][0] = true; 25 for (int i=0; i<n; i++) 26 { 27 for (int j=0; j<=K; j++) 28 { 29 for (int k=0; j-k*a[i]>=0 && k<=m[i]; k++) 30 { 31 dp[i+1][j] |= dp[i][j-k*a[i]]; 32 } 33 } 34 } 35 if (dp[n][K]) cout << "Yes" << endl; 36 else cout << "No" << endl; 37 }
-
上面的做法并不够好,一般来讲,用DP来求取bool结果的话会有不少浪费,同样的复杂度通常能获得更多的信息,在这个问题中,我们不光求出能否得到目标的和数,同时把得到时ai这个数还剩下多少个计算出来,这样就可以减少复杂度:
dp[i+1][j] := 用前i+1种数加和得到j时第i+1种数(编号为i)最多能剩余多少个(不能加和得到j的情况下为-1)
按照如上所述定义递推关系,这样如果前i-1个数加和能得到j的话,第i个数就可以留下mi个。此外,前i+1种数加和出j-ai时第i+1种数还剩下k(k>0)的话,用这i+1种数加和j时第i+1种数就能剩下k-1个,由此可以得出下面的递推式:
dp[i+1][j] = ① mi (dp[i][j]>=0)
② -1 (j<ai或者dp[i+1][j-ai]≤0)
③dp[i+1][j-ai]-1 (其它情况下)
这样,只要看dp[n][K]≥0是否成立,就可以知道答案了,时间复杂度是O(nK),如果使用一维数组还可以将空间压缩:1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 int n,K; 7 int * a; 8 int * m; 9 int * dp; 10 11 int main() 12 { 13 cin >> n >> K; 14 a = new int[n]; 15 m = new int[n]; 16 for (int i=0; i<n; i++) 17 cin >> a[i] >> m[i]; 18 dp = new int[K+1]; 19 memset(dp,-1,sizeof(int)*(K+1)); 20 dp[0] = 0; 21 for (int i=0; i<n; i++) 22 { 23 for (int j=0; j<=K; j++) 24 { 25 if (dp[j]>=0) dp[j]=m[i]; 26 else if(j<a[i] || dp[j-a[i]]<=0) dp[j]=-1; 27 else dp[j]=dp[j-a[i]]-1; 28 } 29 } 30 if (dp[K]>=0) cout << "Yes" << endl; 31 else cout << "No" << endl; 32 }
- 这个问题可以用DP来做,不过如何定义递推关系会影响到最终的复杂度。先看这个定义:
最长上升子序列问题
- 问题描述:有一个长为n的数列a0,a1,…,an-1.求出这个序列中最长的上升子序列的长度。
- 限制条件:
- 1≤n≤1000
- 0≤ai≤1000000
- 分析:这个问题是被称作最长上升子序列(LCS,Longest Increasing Subsequence)的著名问题。这一问题通过使用DP可以很有效率地解决。
- 首先,先建立递推关系:
定义dp[i] := 以ai为末尾的最长上升子序列的长度
以ai结尾的上升子序列是
①只包含ai的子序列
②在满足j<i并且ai<aj的以aj为结尾的上升子序列末尾,追加上ai后得到的子序列
这二者之一,这样不难得到递推关系:dp[i] = max{1,dp[j]+1|j<i且aj<ai}
时间复杂度为O(n2)1 #include <iostream> 2 3 using namespace std; 4 5 int n; 6 int * a; 7 int * dp; 8 int ans=1; 9 10 int main() 11 { 12 cin >> n; 13 a = new int[n]; 14 dp = new int[n]; 15 for (int i=0; i<n; i++) 16 { 17 cin >> a[i]; 18 dp[i] = 1; 19 } 20 for (int i=1; i<n; i++) 21 { 22 for (int j=0; j<i; j++) 23 { 24 if (a[j]<a[i] && (dp[j]+1)>dp[i]) dp[i]=dp[j]+1; 25 } 26 if (dp[i]>ans) ans = dp[i]; 27 } 28 cout << ans << endl; 29 }
- 那么,有没有效率更高效的方法呢?前面我们利用DP求取针对最末位的元素的LIS,如果子序列的长度相同,那么最末位的元素较小的在之后会更加有优势,所以我们再反过来用DP针对相同长度情况下最小的末尾元素进行求解:
dp[i] := 长度为i+1的上升子序列中末尾元素的最小值(不存在的话就是INF)
最开始的话dp[i]都初始化为INF,然后由前到后逐个考虑数组a的元素,对于每个aj,如果i=0或者dp[i-1]<aj的话,就用dp[i]=min(dp[i],aj)进行更新,最终找出使得dp[i]<INF的最大的i+1就是结果了。复杂度和之前一样为O(n2),但这一算法还可以进一步优化,显然dp数组中除INF之外是单调递增的,所以可以知道对于每个aj最多只需要1次更新,对于这次更新的位置,可二分搜索,这样就可以在O(nlogn)时间内出结果了上面的代码使用了lower_bound这个STL函数,这个函数从已排好序的序列a中利用二分搜索找出指向满足ai≥k的ai的最小的指针1 #include <iostream> 2 #include <algorithm> 3 4 using namespace std; 5 6 const int INF = 0x3FFFFFF; 7 int n; 8 int * a; 9 int * dp; 10 int ans=1; 11 12 int main() 13 { 14 cin >> n; 15 a = new int[n]; 16 dp = new int[n]; 17 for (int i=0; i<n; i++) 18 { 19 cin >> a[i]; 20 dp[i] = INF; 21 } 22 for (int i=0; i<n; i++) 23 { 24 *(lower_bound(dp,dp+n,a[i])) = a[i]; 25 } 26 cout << lower_bound(dp,dp+n,INF)-dp << endl; 27 }
类似的函数还有upper_bound,这一函数求出的是指向满足ai>k的ai的最小的指针
有了它们,比如长度为n的有序数组a中的k的个数,可以这样方便地求出:upper_bound(a,a+n,k)-lower_bound(a,a+n,k)
- 首先,先建立递推关系:
有关计数问题的DP
划分数
- 问题描述:有n个无区别的物品,将它们划分成不超过m组,求出划分方法数模M的余数
- 限制条件
- 1≤m≤n≤1000
- 2≤M≤10000
- 这样的划分被称作n的m划分,特别地,m=n时称作n的划分数。(
科普:将基数为n的集合划分为恰好k个非空集的方法的数目称为第二类Stirling数,而将基数为n的集合划分为任意个非空集的方法的数目称为Bell数)
DP不仅对于求解最优问题有效,对于各种排列组合的个数、概率或者期望之类的计算同样很有用。
分析:
定义:dp[i][j] :=j的i划分的总数
根据这一定义写出递推关系,将j个划分i份的话,可以先取k个,然后将剩下的j-k个分成i-1份,这个想法看起来很自然,但是错误的,因为有大量的重复计数!
寻找别的递推关系,考虑n的m划分ai(∑ai=n),如果对于每个i都有ai>0,那么{ai-1}就对应了n-m的m划分,另外,如果存在ai=0,那么这就对应了n的m-1划分,综上,我们可以写出这样的递推关系:
dp[i][j]=dp[i][j-i]+dp[i-1][j]
这个递推式可以不重复地计算所有的划分,复杂度为O(nm),像这样需要在计数问题中解决重复计算问题时,需要特别小心1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 int n,m,M; 7 int * a; 8 int **dp; 9 10 int main() 11 { 12 cin >> n >> m >> M; 13 a = new int[n]; 14 dp = new int*[m+1]; 15 for (int i=0; i<=m; i++) 16 { 17 dp[i]=new int[n+1]; 18 memset(dp[i],0,sizeof(int)*(n+1)); 19 } 20 dp[0][0]=1; 21 for (int i=1; i<=m; i++) 22 { 23 for (int j=0; j<=n; j++) 24 { 25 if (j>=i) dp[i][j]=(dp[i][j-i]+dp[i-1][j])%M; 26 else dp[i][j]=dp[i-1][j]; 27 } 28 } 29 cout << dp[m][n] << endl; 30 }
多重集组合数
- 问题描述:有n种物品,第i种物品有ai个,不同种类的物品可以互相区分,但相同种类的无法区分,从这些物品中取出m个的话,有多少种取法?求出方案数模M的余数。
- 限制条件:
- 1≤n≤1000
- 1≤m≤1000
- 1≤ai≤1000
- 2≤M≤10000
- 分析:为了不重复计数,同一种类的物品最好一次性处理好。
按照如下方式进行定义:
dp[i+1][j] := 从前i+1种物品中取出j个的组合总数
为了从前i种物品中取出j个,可以从前i-1种物品中取出j-k个,再从第i种物品中取出k个添加进来,所以递推关系为:dp[i+1][j] = ∑dp[i][j-k](0≤k≤min(j,a[i])),
直接计算的话,复杂度是O(nm2),比较高,接下来一波常规操作,将递推式进行变形:
由于 ∑dp[i][j-k](0≤k≤min(j,a[i]))=(∑dp[i][j-1-k])+dp[i][j]-dp[i][j-1-a[i]] (0≤k≤min(j-1,a[i])) (其中a[i]≤j-1)
∑dp[i][j-k](0≤k≤min(j,a[i]))=(∑dp[i][j-1-k])+dp[i][j] (0≤k≤min(j-1,a[i])) (其中a[i]≥j)
所以,可将递推式变形为:
dp[i+1][j] = dp[i+1][j-1]+dp[i][j]-dp[i][j-1-a[i]] (其中a[i]≤j-1)
dp[i+1][j] = dp[i+1][j-1]+dp[i][j] (其中a[i]≥j)
1 #include <iostream> 2 #include <cstring> 3 4 using namespace std; 5 6 int n,m,M; 7 int * a; 8 int **dp; 9 10 int main() 11 { 12 cin >> n >> m >> M; 13 a = new int[n]; 14 dp = new int*[n+1]; 15 for (int i=0; i<n; i++) cin >> a[i]; 16 for (int i=0; i<=n; i++) 17 { 18 dp[i]=new int[m+1]; 19 memset(dp[i],0,sizeof(int)*(m+1)); 20 } 21 for (int i=0; i<=n; i++) 22 { 23 dp[i][0] = 1; 24 } 25 for (int i=0; i<n; i++) 26 { 27 for (int j=1; j<=m; j++) 28 { 29 if (j-1>=a[i]) dp[i+1][j]=(dp[i+1][j-1]+dp[i][j]-dp[i][j-1-a[i]]+M)%M; 30 else dp[i+1][j]=(dp[i+1][j-1]+dp[i][j])%M; 31 } 32 } 33 cout << dp[n][m] << endl; 34 }