动态规划(Dynamic Programming,DP)是运筹学的一个分支,是求解决策过程最优化的过程.
应用条件 1.最优化问题
2.重叠子问题
3.子问题空间不能太大,DP通常情况下需要求解每一个子问题的最优解来组合得到原问题的最优解。
最优子结构 无后效性 子问题重叠
基本流程 1.刻画一个最优解的结构特征(状态设计)
2.递归的定义最优解的值(状态转移方程设计)
3.计算最优解的值,通常采用自底向上(递推)的方法(处理各种边界条件)
背包家族 01背包 你有一个容量固定为M的背包,以及N个物品。每一个物品具有一个固定的体积v和价值w。你需要在不超过背包容量的前提下最大化带走物品的价值。
状态转移方程 DP[i][j]=max(DP[i-1][j],DP[i-1][j-w[i]]+v[i])
第 i 个物品不在最优方案中(不取第 i 个物品),问题变成了考虑了前 i-1 个物品,总体积不超过 j 时的情况。当前,一种潜在的最优方案是前 i-1个物品,总体积不超过 j 时的最优方案 ;
第 i 个物品在最优方案中(取了第 i 个物品),问题变成了考虑了前 i-1 个物品,总体积不超过 j-v[i] 时的情况;当前,另一种潜在的最优方案是在考虑了前 i-1 个物品,总体积不超过 j-v[i] 时的最优方案的基础上再加上第 i 个物品,对应的收益是前 i-1 个物品,总体积不超过 j-v[i]时的最大收益 + 第 i 个物品的收益w[i]
根据题目会有一些非法状态,需要将其赋一个特殊值 (例如题目要求改为恰好装满容量为M的背包那么只有DP[0][0]为0,其他为负无穷)
记忆化搜索 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 CUT(p,cnt) if (cnt==0 )return 0 ; q=-∞; for i = 1 to cnt q=max (q,p[i]+CUT (p,cnt-i)); p[cnt]=q; return p[cnt]; CUT(p,vis,cnt) if (cnt==0 )return 0 ; if (vis[cnt])return p[cnt]; q=-∞; for i = 1 to cnt q=max (q,p[i]+CUT (p,cnt-i)); p[cnt]=q; vis[cnt]=true ; return p[cnt];
优化 dfs暴搜(MLE/TLE) ——> 优化
剪枝 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <bits/stdc++.h> using namespace std;int n, m, f[1001 ][1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%d%d" , &v[i], &w[i]); } memset (f, 0 , sizeof (f)); for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= m; j++) { f[i][j]=f[i-1 ][j]; if (j>=v[i]) f[i][j]=max (f[i-1 ][j],f[i-1 ][j-v[i]]+w[i]); } } printf ("%d\n" , f[n][m]); }
滚动数组 两个大小为m的数组 在我们从小到大枚举 i 的过程中,前 i-2 行的值其实是不用记的(它们不会对后面产生影响),所以我们可以用两个大小为 m 的数组 f, g 来进行所有状态的计算,其中 f 用来记录 i - 1 行的值,g 用来计算第 i 行的值,当第 i 行计算完成的时候,g 就变成了新的 f,我们用它来计算新一行(第 i + 1)的 g 值。在执行过程中,f, g 两个数组周而复始、循环往复,这就是滚动数组的思想啦!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <bits/stdc++.h> using namespace std;int n, m, f[1001 ], g[1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%d%d" , &v[i], &w[i]); } memset (f, 0 , sizeof (f)); for (int i = 1 ; i <= n; i++) { memset (g, 0 , sizeof (g)); for (int j = 0 ; j <= m; j++) { if (j < v[i]) g[j] = f[j]; else g[j] = max (f[j], f[j - v[i]] + w[i]); memcpy (f, g, sizeof (g)); } } printf ("%d\n" , f[m]); }
一个大小为m的数组 假设现在 p 数组中记录的是第 i - 1 行的值,我们要计算第 i 行的值。 然后我们从右到左 依次计算每个位置新的值(第 i 行的值)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 #include <bits/stdc++.h> using namespace std;int n, m, p[1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%d%d" , &v[i], &w[i]); } memset (p, 0 , sizeof (p)); for (int i = 1 ; i <= n; i++) { for (int j = m; j >= v[i]; j--) { p[j] = max (p[j], p[j - v[i]] + w[i]); } } printf ("%d\n" , p[m]); }
完全背包 完全背包和 01 背包的不同点是,在 01 背包问题中每种物品只能取一次 ,而在完全背包问题中物品没有取多少次的限制,想取多少次就取多少次 。解决完全背包问题的思路和 01 背包问题的思路非常像。
第 i 种物品一个都没取,问题变成了考虑了前 i − 1 种物品,总体积为 j 时的情况;这一种情况是和 01 背包问题完全一样的;
第 i 种物品取了,问题变成了考虑了前 i 种物品,总体积为 j-v[i] 时的情况;当我们取了一个物品以后,第 i 种物品还可以继续取,所以我们仍然要继续考虑前 i 种物品的情况; 作为对比,在 01 背包问题中,如果第 i 个物品取了,由于每种物品最多取一次,问题会变成考虑了前 i - 1 种物品的情况
状态转移方程 DP[i][j]=max(DP[i-1][j],DP[i][j-v[i]]+w[i])
n个大小为m的数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 #include <bits/stdc++.h> using namespace std;int n, m, f[1001 ][1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) { scanf ("%d%d" , &v[i], &w[i]); } memset (f, 128 , sizeof (f)); f[0 ][0 ] = 0 ; for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= m; j++) { if (j < v[i]) f[i][j] = f[i - 1 ][j]; else f[i][j] = max (f[i - 1 ][j], f[i][j - v[i]] + w[i]); } } int ans = 0 ; for (int i = 0 ; i <= m; i++) ans = max (ans, f[n][i]); printf ("%d\n" , ans); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #include <bits/stdc++.h> using namespace std;int n, m, f[1001 ][1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) scanf ("%d%d" , &v[i], &w[i]); memset (f, 0 , sizeof (f)); for (int i = 1 ; i <= n; i++) { for (int j = 0 ; j <= m; j++) { if (j < v[i]) f[i][j] = f[i - 1 ][j]; else f[i][j] = max (f[i - 1 ][j], f[i][j - v[i]] + w[i]); } } printf ("%d\n" , f[n][m]); }
一个大小为m的数组
第 i 个物品在最优方案中的情况下,用到了 f[i][j-v[i]] ,而 j-v[i] 一定小于 j, 第 i 个物品不在最优方案中的情况下,用到了 f[i - 1][j]
假设现在 p 数组中记录的是第 i - 1 行的值,我们要计算第 i 行的值。 和 01 背包问题相反,我们从左到右 依次计算每个位置新的值(第 i 行的值)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 #include <bits/stdc++.h using namespace std;int n, m, p[1001 ], v[1001 ], w[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) scanf ("%d%d" , &v[i], &w[i]); memset (p, 0 , sizeof (p)); for (int i = 1 ; i <= n; i++) for (int j = v[i]; j <= m; j++) p[j] = max (p[j], p[j - v[i]] + w[i]); printf ("%d\n" , p[m]); }
多重背包 和 01 背包问题不一样的是,第 i 个物品不是只能取一次,而是可以取 l[i] 次
把它转换成一个 01 背包问题 一个可以取 l[i] 次的物品,等价于 l[i] 个只能取一次的物品
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <bits/stdc++.h> using namespace std;int n, m, p[1001 ], v[1001 ], w[1001 ], l[1001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) scanf ("%d%d%d" , &v[i], &w[i], &l[i]); memset (p, 0 , sizeof (p)); for (int i = 1 ; i <= n; i++) for (int k = 1 ; k <= l[i]; k++) for (int j = m; j >= v[i]; j--) p[j] = max (p[j], p[j - v[i]] + w[i]); printf ("%d\n" , p[m]); }
二进制拆分法 定理:令 t 为最大的 i 满足 2^(i+1)-1<=n ,我们从 1,2,4,…,2^t,n-2^(t+1)+1 中选一些数字相加(可以不选),可以得出任意 [0,n] 内的值.(每个数字只能用一次)
对于在 [0,2^(t+1)] 中的值,我们可以直接从 1,2,4,…,2^t 中选一些数字相加得到; 对于在 [2^(t+1),n] 内的值,我们取 n-2^(t+1)+1 后, 令 [2^(t+1),n] 同减去 n-2^(t+1)+1 ,得 [2^(t+2)-n-1,2^(t+1)] , 则剩下的数字在 [2^(t+2)-n-1,2^(t+1)] 内,可以从 1,2,4,…,2^t 中选一些数字相加得到.
举个例子,当 l[i]=8 时,我们会把第 i 个物品拆成 4 个物品: 取 1 次第 i 种物品的打包; 取 2 次第 i 种物品的打包; 取 4 次第 i 种物品的打包; 取 1 次第 i 种物品的打包.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 #include <bits/stdc++.h> using namespace std;int n, m, p[2001 ], v[2001 ], w[2001 ], l[2001 ];int main () { scanf ("%d%d" , &n, &m); for (int i = 1 ; i <= n; i++) scanf ("%d%d%d" , &v[i], &w[i], &l[i]); memset (p, 0 , sizeof (p)); for (int i = 1 ; i <= n; i++) { int res = l[i]; for (int k = 1 ; k <= res; res -= k, k *= 2 ) for (int j = m; j >= v[i] * k; j--) p[j] = max (p[j], p[j - v[i] * k] + w[i] * k); for (int j = m; j >= v[i] * res; j--) p[j] = max (p[j], p[j - v[i] * res] + w[i] * res); } printf ("%d\n" , p[m]); }
分组背包 二维背包 LIS 最长上升子序列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 #include <bits/stdc++.h> using namespace std;int const N=5e3 +10 ;int n,a[N],f[N],pre[N];int main () { cin>>n; for (int i=1 ;i<=n;++i) { cin>>a[i]; } for (int i=1 ;i<=n;++i) { f[i]=1 ; for (int j=1 ;j<i;++j) { if (a[j]<a[i]) { if (f[j]+1 >f[i]) pre[i]=j; f[i]=max (f[i],f[j]+1 ); } } } int ans=-1 ,ind=0 ; for (int i=1 ;i<=n;++i) { if (f[i]>ans) ind=i; ans=max (ans,f[i]); } cout<<ans<<endl; function<void (int x)>dfs=[&](int x) { if (pre[x]=-1 ) { cout<<a[x]<<" " ; return ; } dfs (pre[x]); cout<<a[x]<<" " ; }; dfs (ind); return 0 ; }
动态规划具有 判断性继承思想
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <bits/stdc++.h> using namespace std;int const N=5e3 +10 ;int n,a[N],dp[N];int dfs (int i) { if (dp[i]!=-1 ) return dp[i]; int ans=1 ; for (int j=1 ;j<i;++j) { if (a[j]<a[i]) ans=max (ans,dfs (j)+1 ); } } int main () { memset (dp,-1 ,sizeof (dp)); cin>>n; for (int k=1 ;k<=n;++k) { cin>>a[k]; } int ans=0 ; for (int i=1 ;i<=n;++i) { ans=max (ans,dfs (i)); } cout<<ans<<endl; } ```C++ #include <bits/stdc++.h> using namespace std;const int INF = 0x7fffffff ; const int N = 1e5 +10 ;long long a[N],f[N]; int main () { int n; cin>>n; for (int i=1 ;i<=n;i++) { cin>>a[i]; f[i]=INF; } f[1 ]=a[1 ]; int len=1 ; for (int i=2 ;i<=n;i++) { int l=0 ,r=len,mid; if (a[i]>f[len]) f[++len]=a[i]; else { while (l<r) { mid=(l+r)/2 ; if (f[mid]>a[i]) r=mid; else l=mid+1 ; } f[l]=min (a[i],f[l]); } } cout<<len; return 0 ; }
LCS 最长公共子序列
此题用n方时间复杂度会被10的5次方卡死 要考虑 nlogn 做法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <bits/stdc++.h> using namespace std;const int N =1e2 +10 ;int dp[N][N],a[N],b[N];int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); int n,m; cin>>n>>m; for (int i=0 ;i<n;i++) { cin>>a[i]; } for (int i=0 ;i<m;i++) { cin>>b[i]; } for (int i=0 ;i<n;i++) { for (int j=0 ;j<m;j++) { dp[i][j]=max (dp[i-1 ][j],dp[i][j-1 ]); if (a[i]==b[j]) dp[i][j]=max (dp[i][j],dp[i-1 ][j-1 ]+1 ); } } cout<<dp[n-1 ][m-1 ]; return 0 ; }
将LCS转化为LIS问题,降低时间复杂度
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 #include <bits/stdc++.h> using namespace std;const int N =1e5 +10 ;int m[N],dp[N],a[N],b[N];int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); int n; cin>>n; for (int i=1 ;i<=n;i++) { cin>>a[i]; m[a[i]]=i; } for (int i=1 ;i<=n;i++) { cin>>b[i]; dp[i]=0x7fffffff ; } int len=0 ; dp[0 ]=0 ; for (int i=1 ;i<=n;i++) { int l=0 ,r=len,mid; if (m[b[i]]>dp[len]) { dp[++len]=m[b[i]]; } else { while (l<r) { mid=(l+r)/2 ; if (dp[mid]>m[b[i]]) { r=mid; } else { l=mid+1 ; } } dp[l]=min (m[b[i]],dp[l]); } } cout<<len; return 0 ; }
区间dp 石子合并
铺垫————
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <bits/stdc++.h> using namespace std;int n,a[501 ],s[501 ];int solve (int l,int r) { if (l==r) return 0 ; int ans=1 <<30 ; for (int m=l;m<r;m++) { ans=min (ans,solve (l,m)+solve (m+1 ,r)); } return ans+s[r]-s[l-1 ]; } int main () { scanf ("%d" ,&n); for (int i=1 ;i<=n;i++) { scanf ("%d" ,&a[i]); } for (int i=1 ;i<=n;i++) { s[i]=s[i-1 ]+a[i]; } printf ("%d\n" ,solve (1 ,n)); }
板子———— 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 #include <bits/stdc++.h> using namespace std;const int N =1e3 +10 ;void solve () { int n; cin>>n; vector<int > num (n*2 +1 ) ,sum (n*2 +1 ) ; for (int i=1 ;i<=n;i++) { cin>>num[i]; num[i+n]=num[i]; } vector<vector<int >> minn (n*2 +1 ,vector <int >(n*2 +1 ,0x3f3f3f )), maxx (n*2 +1 ,vector <int >(n*2 +1 )); for (int i=1 ;i<=n*2 ;i++) { sum[i]=sum[i-1 ]+num[i]; minn[i][i]=0 ; } for (int len=1 ;len<=n;len++) { for (int i=1 ;i+len<=2 *n;i++) { for (int k=i;k<i+len;k++) { int j=i+len; minn[i][j]=min (minn[i][j],minn[i][k]+minn[k+1 ][j]+sum[j]-sum[i-1 ]); maxx[i][j]=max (maxx[i][j],maxx[i][k]+maxx[k+1 ][j]+sum[j]-sum[i-1 ]); } } } int ans=0x3f3f3f3f ; for (int i=1 ;i<=n;i++) { ans=min (ans,minn[i][i+n-1 ]); } cout<<ans<<endl; ans=0 ; for (int i=1 ;i<=n;i++) { ans=max (ans,maxx[i][i+n-1 ]); } cout<<ans<<endl; return ; } signed main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); int t=1 ; while (t--) { solve (); } return 0 ; }
树形dp 有点权, 记录点e[], 找根in[], dp[][0/1]进行选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 #include <bits/stdc++.h> using namespace std;const int N =1e3 +10 ;void solve () { int n; cin>>n; vector<int > num (n+1 ) ; for (int i=1 ;i<=n;i++) { cin>>num[i]; } vector<vector<int >> e (n+1 ); vector<int > in (n+1 ) ; for (int i=1 ;i<n;i++) { int v,u; cin>>v>>u; e[u].push_back (v); in[v]++; } vector<vector<int >> dp (n+1 ,vector <int >(2 )); vector<int > vis (n+1 ) ; for (int i=1 ;i<=n;i++) { dp[i][1 ]=num[i]; } auto dfs=[&](auto dfs,int u)->void { if (vis[u]) return ; vis[u]=1 ; for (auto v:e[u]) { dfs (dfs,v); dp[u][0 ]+=max (dp[v][1 ],dp[v][0 ]); dp[u][1 ]+=dp[v][0 ]; } }; for (int i=1 ;i<=n;i++) { if (in[i]==0 ) { dfs (dfs,i); cout<<max (dp[i][0 ],dp[i][1 ]); } } return ; } int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); int t=1 ; while (t--) { solve (); } return 0 ; }
二叉苹果树 有边权,链表记录边e[],记录节点以下边数目sz[]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 #include <bits/stdc++.h> using namespace std;const int N =1e2 +10 ;int n,q,f[N][N]; struct edge { int w,to,nxt; }e[N<<1 ]; int tot,head[N];inline void add (int u,int v,int w) { e[tot]={w,v,head[u]}; head[u]=tot++; } int sz[N];void dfs (int u,int fa) { for (int i=head[u];~i;i=e[i].nxt) { int v=e[i].to; if (v==fa) continue ; dfs (v,u); sz[u]+=sz[v]+1 ; for (int j=min (sz[u],q);j;--j) { for (int k=min (sz[v],j-1 );k>=0 ;--k) { f[u][j]=max (f[u][j],f[u][j-k-1 ]+f[v][k]+e[i].w); } } } } int main () { ios::sync_with_stdio (0 ); cin.tie (0 ); cout.tie (0 ); memset (head,-1 ,sizeof (head)); cin>>n>>q; for (int i=1 ;i<n;i++) { int u,v,w; cin>>u>>v>>w; add (u,v,w); add (v,u,w); } dfs (1 ,-1 ); cout<<f[1 ][q]<<endl; return 0 ; }
状压dp
将状态压缩为整数来达到优化转移的目的