参考《挑战程序设计竞赛》p51
https://www.cnblogs.com/Ymir-TaoMee/p/9419377.html
01背包问题
- 问题描述:有n个重量和价值分别为wi、vi的物品,从这些物品中挑选出总重量不超过W的物品,求所有挑选方案中价值总和的最大值。
input:
4
5
2 3
1 2
3 4
2 2
output:
7(选择第0、1、 3号物品)
朴素解法:
c++版:
#include <iostream> using namespace std; int n,W; int *w,*v;//数组的指针 int max(int x, int y) { if (x>y) return x; return y; } int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体,i从0开始 { int res; if (i==n) res=0;//没有物品了 else if (j<w[i]) res=rec(i+1,j);//重量j小于该组物品的重量,不能取 else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]);//重量j大于该组物品的重量,能取;挑选和不挑选都尝试一下 return res; } int main() { cin >> n >> W;//n组物品,W:总重量 w = new int[n]; v = new int[n]; for (int i=0; i<n; i++) cin >> w[i] >> v[i]; cout << rec(0,W) << endl; }
Java版本
package 记忆化搜索; import java.util.Scanner; public class Main { static int[] w, v; public static void main(String[] args) { Scanner sc=new Scanner(System.in); int n=sc.nextInt(); int W=sc.nextInt(); w = new int[n]; v = new int[n]; for (int i=0; i<n; i++) { w[i]=sc.nextInt(); v[i]=sc.nextInt(); } System.out.println(rec(0,W)); } private static int rec(int i, int j) { if (i==w.length) { return 0; } if (j<w[i]) { return rec(i+1, j); } int a=rec(i+1, j); int b=rec(i+1, j-w[i])+v[i]; return Math.max(a, b); } }
这种方法的搜索深度是n,而且每一层的搜索都需要两次分支,最坏就需要O(2n)的时间。当n比较大时就没办法解了。所以要怎么办才好呢?为了优化之前的算法,我们看一下针对样例输人的情形下rec递归调用的情况。以下是rec(i,j)的模拟情况,i:第几组物品,j:重量
如图所示,rec以(3,2)为 参数调用了两次。如果参数相同,返回的结果也应该相同,于是第二次调用时已经知道了结果却白白浪费了计算时间。让我们在这里把第1次计算时的结果记录下来,省略掉第二次以后的重复计算试试看。
c++版本:
#include <iostream> #include <cstring> using namespace std; int n,W; int *w,*v; int **dp; int max(int x, int y) { if (x>y) return x; return y; } int rec(int i, int j)//从数组下标为i的物品开始往后挑选总重小于j的物体 { if (dp[i][j]>=0) return j[i[dp]];//和dp[i][j]的意义一样 int res; if (i==n) res=0; else if (j<w[i]) res=rec(i+1,j); else res=max(rec(i+1,j),rec(i+1,j-w[i])+v[i]); dp[i][j] = res; return res; } int main() { cin >> n >> W; w = new int[n]; v = new int[n]; dp = new int*[n+1]; for (int i=0; i<=n; i++) { dp[i] = new int[W+1]; memset(dp[i],-1,sizeof(int)*(W+1)); } for (int i=0; i<n; i++) cin >> w[i] >> v[i]; cout << rec(0,W) << endl; }
Java版本:
package 记忆化搜索; import java.util.Arrays; import java.util.Scanner; public class Main { static int[] w, v; static int[][] dp; public static void main(String[] args) { Scanner sc=new Scanner(System.in); int n=sc.nextInt(); int W=sc.nextInt(); w = new int[n]; v = new int[n]; for (int i=0; i<n; i++) { w[i]=sc.nextInt(); v[i]=sc.nextInt(); } dp=new int[n+1][W+1]; for (int i = 0; i < dp.length; i++) { Arrays.fill(dp[i], -1); } System.out.println(rec(0,W)); } private static int rec(int i, int j) { if (dp[i][j]>=0) { return dp[i][j]; } if (i==w.length) { return 0; } if (j<w[i]) { return rec(i+1, j); } int a=rec(i+1, j); int b=rec(i+1, j-w[i])+v[i]; int res=Math.max(a, b); dp[i][j]=res; return res; } }
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]) (其它情况下)
c++版本解法:
#include <iostream> #include <cstring> using namespace std; int n,W; int *w,*v; int **dp; int max(int x, int y) { if (x>y) return x; return y; } int main() { cin >> n >> W; w = new int[n]; v = new int[n]; dp = new int*[n+1]; for (int i=0; i<=n; i++) { dp[i] = new int[W+1]; memset(dp[i],0,sizeof(int)*(W+1)); } for (int i=0; i<n; i++) cin >> w[i] >> v[i]; for (int i=0; i<n; i++) { for (int j=0; j<=W; j++) { if (j<w[i]) dp[i+1][j]=dp[i][j]; else dp[i+1][j] = max(dp[i][j],dp[i][j-w[i]]+v[i]); } } cout << dp[n][W] << endl; }
java版本:
参考代码:https://www.acwing.com/problem/content/submission/code_detail/3617/
import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner scanner=new Scanner(System.in); int n=scanner.nextInt(); int m=scanner.nextInt(); int v[]=new int[n+1]; int w[]=new int[n+1]; for (int i = 0; i <n; i++) { v[i]=scanner.nextInt(); w[i]=scanner.nextInt(); } int f[][]=new int[n+1][m+1]; for (int i = 0; i <n ; i++) { for (int j = 0; j <=m ; j++) { if(j<v[i]) f[i+1][j]=f[i][j]; else f[i+1][j]=Math.max(f[i][j],f[i][j-v[i]]+w[i]); } } System.out.println(f[n][m]); } }
完全背包问题
- 问题描述:有n种重量和价值分别为wi,vi的物品,从这些物品中挑选总重量不超过W的物品,求出挑选物品价值总和的最大值,在这里,每种物品可以挑选任意多件。
分析:这次同一种类的物品可以选择任意多件了,尝试着写出递推关系:
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}
#include <iostream> #include <cstring> using namespace std; int n,W; int * w; int * v; int **dp; int max(int x, int y) { if (x>y) return x; return y; } int main() { cin >> n >> W; w = new int[n]; v = new int[n]; for (int i=0; i<n; i++) { cin >> w[i] >>v[i]; } dp = new int*[n+1]; for (int i=0; i<=n; i++) { dp[i] = new int[W+1]; memset(dp[i],0,sizeof(int)*(W+1)); } for (int i=0; i<n; i++) { for (int j=0; j<=W; j++) { for (int k=0; k*w[i]<=j; k++) { dp[i+1][j] = max(dp[i+1][j],dp[i][j-k*w[i]]+k*v[i]); } } } cout << dp[n][W] << endl; }
上面的程序是三重循环的,关于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}) //将k=0;k>=1的情况分开
= max(dp[i][j],max{dp[i][(j-w[i])-k*w[i]]+k*v[i]|k≥0}+v[i])//令k=k+1
= max(dp[i][j],dp[i+1][j-w[i]]+v[i]) //因为dp[i+1][j-w[i]]=dp[i][(j-w[i])-k*w[i]]+k*v[i]
即:dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i])
这样处理之后,就不需要关于k的循环了,现在的复杂度为O(nW):
#include <iostream> #include <cstring> using namespace std; int n,W; int * w; int * v; int **dp; int max(int x, int y) { if (x>y) return x; return y; } int main() { cin >> n >> W; w = new int[n]; v = new int[n]; for (int i=0; i<n; i++) { cin >> w[i] >>v[i]; } dp = new int*[n+1]; for (int i=0; i<=n; i++) { dp[i] = new int[W+1]; memset(dp[i],0,sizeof(int)*(W+1)); } for (int i=0; i<n; i++) { for (int j=0; j<=W; j++) { if (j<w[i]) dp[i+1][j] = dp[i][j]; else dp[i+1][j] = max(dp[i][j],dp[i+1][j-w[i]]+v[i]); } } cout << dp[n][W] << endl; }
完全背包问题的变种
LeetCode .322
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
示例 1:
输入: coins = [1, 2, 5], amount = 11
输出: 3
解释: 11 = 5 + 5 + 1
示例 2:
输入: coins = [2], amount = 3
输出: -1
最长公共子序列问题
- 问题描述:给定两个字符串s1s2…sn和t1t2…tn。求这两个字符串最长的公共子序列的长度。
input:
s = "abcd"
output:
3("bcd")
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的长度
c++版本:
#include <iostream> #include <cstring> using namespace std; int n,m; char * s; char * t; int **dp; int max(int x, int y) { if (x>y) return x; return y; } int main() { cin >> n >> m; s = new char[n+1]; t = new char[m+1]; for (int i=0; i<n; i++) { cin >> s[i]; } for (int i=0; i<m; i++) { cin >> t[i]; } dp = new int*[n+1]; for (int i=0; i<=n; i++) { dp[i] = new int[m+1]; memset(dp[i],0,sizeof(int)*(m+1)); } for (int i=0; i<n; i++) { for (int j=0; j<m; j++) { if (s[i]==t[j]) dp[i+1][j+1]=dp[i][j]+1; else dp[i+1][j+1]=max(dp[i+1][j],dp[i][j+1]); } } cout << dp[n][m] << endl; }
Java版本
package 记忆化搜索; import java.util.Scanner; public class Main { public static void main(String[] args) { Scanner sc=new Scanner(System.in); int n=sc.nextInt(); int m=sc.nextInt(); String s=sc.next(); String t=sc.next(); int [][]dp=new int[n+1][m+1]; for (int i=0; i<n; i++) { for (int j=0; j<m; j++) { if (s.charAt(i)==t.charAt(j)) dp[i+1][j+1]=dp[i][j]+1; else dp[i+1][j+1]=Math.max(dp[i+1][j],dp[i][j+1]); } } System.out.println(dp[n][m]); } }