菜狗现在才开始备战蓝桥杯QAQ
编程实现动态规划的状态转移方程时, 务必分清楚阶段、 状态与决策, 三者应该按照由外向内的顺序依次循环!! ————蓝书
背包问题
01背包
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N], w[N];
int main () {
int n, V;
cin >> n >>V;
for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
f[0][0] = 0;
for (int i = 1; i <= n; i ++ ) {
for(int j = 0; j <= V; j ++) {
f[i][j] = f[i - 1][j]; // 不选
if(v[i] <= j) {
//如果选上的话 就在上一次加上 w[i]
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); }
}
}
cout << f[n][V] << endl;
return 0;
}
- 01背包优化 :
- f[j]就是所有物品背包容量 j 下的最大价值。即一维f[j]等价于二维
f[n][j]
。 - 为什么一维情况下枚举背包容量需要逆序?在二维情况下,状态
f[i][j]
是由上一轮i - 1的状态得来的,f[i][j]
与f[i - 1][j]
是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]
更新到f[较大体积]
,则有可能本应该用第i-1
轮的状态却用的是第i轮的状态。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int f[N];
int v[N], w[N];
int main () {
int n, V;
cin >> n >>V;
for(int i = 1; i <= n; i++) {
cin >> v[i] >> w[i];
}
f[0] = 0;
for (int i = 1; i <= n; i ++ ) {
for(int j = V; j >= v[i]; j --) {
// f[i][j] = f[i - 1][j]; // 不选
//如果选上的话 就在上一次加上 w[i]
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[V] << endl;
return 0;
}
AcWing 3. 完全背包问题
https://www.acwing.com/problem/content/3/
- 朴素做法 : (TLE)
#include<iostream>
using namespace std;
const int N = 1010;
int n, m;
int dp[N][N], v[N], w[N];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i ++ )
cin >> v[i] >> w[i];
for(int i = 1; i <= n; i ++ )
for(int j = 0; j <= m; j ++ ) // 为什么从0 开始是因为不选的时候 容量就是 0 开始
for(int k = 0; k * v[i] <= j; k ++ )
dp[i][j] = max(dp[i][j], dp[i - 1][j - k * v[i]] + k * w[i]);
cout << dp[n][m] << endl;
}
- 根据错位相减法可得 : 实际上,我们在计算状态方程时不必多一个循环去单独枚举选择第
i
个物品个数
for(int i = 1; i <= n; i++)
for(int j = 0; j <= m; j++)
{
f[i][j] = f[i - 1][j]; // 不选第i个物品
if(j >= v[i]) // 可以选择第i个物品,状态方程见上面推导
f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
}
//优化一维得到
for(int i = 1; i <= n; i++)
for(int j = v[i]; j <= m; j++) {
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
3382. 整数拆分 (完全背包)
- 一个整数总可以拆分为
2
的幂的和。 (直接枚举 所有的2的次幂数) - 完全背包问题 直接删去一维即可
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1000010;
const int mod = 1e9;
// 从i个物品中选 , 且总体积恰好为j 的所有方案的集合
// 需不需要初始化 完全根据定义来写!!
int f[21][N];
//二维写法 会tle
int main () {
int n = 0, m;
cin >> m;
f[0][0] = 1; // 去掉一维
for(int i = 1, v = 1; v <= m; i ++, v *= 2) {
// 去掉 i 枚举 v
n ++;
for(int j = 0; j <= m; j ++) {
f[i][j] = f[i - 1][j]; // f[j] == f[j] 直接去掉
if(j >= v) f[i][j] = (f[i][j] + f[i][j - v]) % mod;
// 直接把 v 搞到 j = v 就行
// 完全背包问题 直接删去一维即可
// 枚举顺序 : 物品 -》 容量 ——》 决策
}
}
cout << f[n][m] << endl;
return 0;
}
// int f[N];
// int main ( ) {
// int n;
// cin >> n;
// f[0] = 1; // 初始化不选的情况
// for(int i = 1; i <= n; i *= 2) {
// for(int j = i; j <= n; j ++) {
// f[j] = (f[j] + f[j - i]) % mod;
// }
// }
// cout << f[n] << endl;
// return 0;
// }
1371. 货币系统(完全背包)
https://www.acwing.com/problem/content/description/1373/
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 10010, V = 30;
long long f[V][N];
int w[N];
int main () {
int v, n;
cin >> v >> n;
for (int i = 1; i <= v; i ++ ) cin >> w[i];
f[0][0] = 1;
for(int i = 1; i <= v; i ++) {
for(int j = 0; j <= n; j ++) {
for(int k = 0; k * w[i] <= j; k ++) {
f[i][j] += f[i - 1][j - w[i] * k];
}
}
}
cout << f[v][n] << endl;
return 0;
}
4. 多重背包问题 I
4877. 最大价值 (多重背包)
- 多重背包 !!
题目链接
#include <iostream>
#include <queue>
#include <algorithm>
#include <string>
#include <cstring>
#include <cmath>
#include <vector>
using namespace std;
#define endl "\n"
const int N = 1000 + 10;
const int M =20;
const int INF = 0x3f3f3f;
const int mod = 1e9 + 7;
typedef pair<int, int> pii;
typedef double db;
typedef long long ll;
#define x first
#define y second
int f[N]; // 编号 容量
//int l[M], h[M], v[M], w[M], g[N];
// 多重背包
void solve() {
int n, m;
int v, w;
cin >> n >> m >> v >> w;
// 初始化 因为0 是必须要选上的!
for(int i = v; i <= n; i ++) f[i] = f[i - v] + w; // 完全背包
for(int i = 1; i <= m; i ++) {
// 01
int l , h;
cin >> l >> h >> v >> w;
for(int j = n; j >= v; j --) {
// 完全背包 体积从大到小枚举
for(int k = 1; k <= l / h && k * v <= j; k ++) {
// 01
f[j] = max(f[j], f[j - v * k] + w * k);
}
}
}
cout <<f[n] << endl;
}
int main () {
ios::sync_with_stdio(false), cin.tie(0), cout.tie(0);
// freopen("input.txt", "r", stdin);
int P = 1;
// cin >> p
while (P -- ) {
solve();
}
return 0;
}
线性DP
272. 最长公共上升子序列
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 3010;
int n;
int a[N], b[N];
int f[N][N];
int main() {
cin >> n;
for (int i = 1; i <= n; i ++ ) cin >> a[i];
for (int i = 1; i <= n; i ++ ) cin >> b[i];
for(int i = 1; i <= n; i ++ ) {
int maxx = 1; // 保存 f(i - 1, j) 中 决策的最大值 !!
for(int j = 1; j <= n; j ++) {
f[i][j] = f[i - 1][j];
if(a[i] == b[j]) f[i][j]= max(maxx ,f[i][j]); // 更新
if(b[j] < a[i]) maxx = max(maxx, f[i - 1][j] + 1); // 更新决策集合
}
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
cout << res << endl;
return 0;
}
271. 杨老师的照相排列
#include <bits/stdc++.h>
// #include <iostream>
using namespace std;
typedef long long ll;
typedef double db;
#define rep(i, a, n) for(int i = a; i <= n; i ++)
#define per(i, a, n) for(int i = n; i <= a; i --)
#define pb push_back;
#define fs first;
#define sz second;
#include <stdlib.h> // atoi
#define debug cout<<"debug"<<"\n"
#define endl "\n";
const int INF = 0x3f3f3f3f;
const int mod=1e9+7;
const int N = 31;
ll f[N][N][N][N][N];
void solve () {
int n;
while(cin >> n , n) {
int s[5] = {
0};
rep(i, 0, n - 1) cin >> s[i];
memset(f, 0 , sizeof f);
f[0][0][0][0][0] = 1;
rep(a, 0, s[0]) {
rep(b, 0, min(s[1], a)) {
rep(c, 0, min(s[2], b)) {
rep(d, 0, min(s[3], c)) {
rep(e, 0, min(s[4], d)) {
// f[a][b][c][d][e]代表从后往前每排人数分别为a, b, c, d, e的所有方案的集合, 其中a >= b >= c >= d >= e; 逆序的
// f[a][b][c][d][e]的值是该集合中元素的数量;
ll &x = f[a][b][c][d][e];
if (a && a - 1 >= b) x += f[a - 1][b][c][d][e];
if (b && b - 1 >= c) x += f[a][b - 1][c][d][e];
if (c && c - 1 >= d) x += f[a][b][c - 1][d][e];
if (d && d - 1 >= e) x += f[a][b][c][d - 1][e];
if (e) x += f[a][b][c][d][e - 1];
// 当a > 0 && a - 1 >= b时,最后一个同学可能被安排在第1排,方案数是f[a - 1][b][c][d][e];
// 当b > 0 && b - 1 >= c时,最后一个同学可能被安排在第2排,方案数是f[a][b - 1][c][d][e];
// 当c > 0 && c - 1 >= d时,最后一个同学可能被安排在第3排,方案数是f[a][b][c - 1][d][e];
// 当d > 0 && d - 1 >= e时,最后一个同学可能被安排在第4排,方案数是f[a][b][c][d - 1][e];
// 当e > 0时,最后一个同学可能被安排在第5排,方案数是f[a][b][c][d][e - 1];
}
}
}
}
}
cout << f[s[0]][s[1]][s[2]][s[3]][s[4]] << endl;
}
}
int main(void) {
freopen("in.txt","r",stdin);
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
最长公共上升子序列
#include <bits/stdc++.h>
// #include <iostream>
using namespace std;
typedef long long ll;
typedef double db;
#define rep(i, a, n) for(int i = a; i <= n; i ++)
#define per(i, a, n) for(int i = n; i <= a; i --)
#define pb push_back;
#define fs first;
#define sz second;
#include <stdlib.h> // atoi
#define debug cout<<"debug"<<"\n"
#define endl "\n";
const int INF = 0x3f3f3f3f;
const int mod=1e9+7;
const int N = 3111;
int a[N], b[N];
int f[N][N];
void solve () {
int n ;
cin >> n;
rep(i, 1, n) cin>> a[i];
rep(i, 1, n) cin>> b[i];
//版本1
// rep(i, 1, n) {
// rep(j, 1 , n) {
// f[i][j] = f[i - 1][j];
// if(a[i] == b[j]) {
// // int maxv = 1; // O(n^ 3)
// // for(int k =1; k < j; k ++) {
// // if(b[j] > b[k]) {
// // maxv = max(maxv, f[i - 1][k] + 1);
// // }
// // f[i][j] = max(maxv, f[i][j]);
// // }
// }
// }
// }
//版本2:
rep(i, 1, n) {
int maxv = 1;
rep(j, 1, n) {
f[i][j] = f[i - 1][j];
if(a[i] == b[j] ) f[i][j] = max(maxv, f[i][j]);
if(a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
}
}
int res = 0;
rep(i, 1, n) {
res = max(res, f[n][i]);
}
cout << res << endl;
}
int main(void){
freopen("in.txt","r",stdin);
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
鸣人的影分身 数字划分
- 写dp 边界 和 循环 条件 都要注意!!
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
int main () {
int t;
cin >> t;
while(t --) {
int n, m; // n 个数 m 总和
cin >> n >> m;
int f[N][N] = {
0};
f[0][0] = 1;
for(int i = 0; i <= n; i ++) {
//枚举和
for(int j = 1; j <= m; j ++) {
//枚举个数
f[i][j] = f[i][j - 1]; // 最小值为 0
if(i >= j) f[i][j] += f[i - j][j]; // 最小值为1
}
}
cout << f[n][m] << endl;
}
return 0;
}
P、糖果
- 动态规划 ---- 闫式DP分析法
状态表示: f[i][j]
集合:从i个数中选总和模k为j的方案
属性:最大值
状态计算: ---- 集合的划分
划分依据:是否包含第i个数
1.包含第i个数 ---- f[i - 1, j - w % k] + w
2.不包含第i个数 ---- f[i - 1][j]
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 110;
int n, k;
int f[N][N];//从i个数中选总数模k为j的方案中的最大值
int main(){
scanf("%d%d", &n, &k);
memset(f, -0x3f, sizeof f);//f[0][1]、f[0][2]···无意义
f[0][0] = 0;//本题求的是最大值而不是方案数,因此为0
for(int i = 1; i <= n; i ++){
int w;
scanf("%d", &w);
for(int j = 0; j <= k; j ++){
f[i][j] = f[i - 1][j];
f[i][j] = max(f[i][j], f[i - 1][(j - w % k + k ) % k] + w);
// j - w % k >> (j - w % k + k ) % k //反正负数越界
}
}
printf("%d\n", f[n][0]);
return 0;
}
区间DP
AcWing 1222. 密码脱落 (区间dp)
#include <cstdio>
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1100;
char s[N];
int f[N][N];//从i个数中选总数模k为j的方案中的最大值
int main(){
scanf("%s", s);
int n = strlen(s);
for(int len = 1; len <= n; len ++) {
for(int l = 0; l + len - 1 < n; l ++) {
int r = l + len - 1;
if(len == 1) f[l][r] = 1;
else {
if(s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2;
if(f[l + 1][r] > f[l][r]) f[l][r] = f[l + 1][r];
if(f[l][r - 1] > f[l][r]) f[l][r] = f[l][r - 1];
}
}
}
cout << n - f[0][n - 1] << endl;
// printf("%d\n", f[n][0]);
return 0;
}
石子合并 (区间dp)
- 区间dp模板题 : 区间抉择
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
- 注意 区间大小从
2
开始 以为1
就是单个石子,没有意义 需要初始化
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 1010;
int n;
int s[N];
int f[N][N];
int main () {
cin >> n;
memset(f, 0x3f, sizeof f); // 初始化
for (int i = 1; i <= n; i ++ ) {
f[i][i] = 0; // 初始化每个边界
cin >> s[i];
s[i] += s[i - 1];
}
for(int len = 2; len <= n; len ++) {
for (int l = 1; l <= n - len + 1; l ++ ) {
int r = l + len - 1;
for(int k = l; k < r; k ++) {
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r] + s[r] - s[l - 1]);
}
}
}
cout << f[1][n] << endl;
return 0;
}
AcWing 1070. 括号配对(区间dp)
思路
- 闫式dp
- 集合划分 f[l][r] 区间选择字母的所有方案
- 属性 min
- 集合划分 (先抉择 再合并):
- (抉择划分) f[i][j] = min(f[i + 1][r - 1] , min (f[i + 1][r], f[i][r - 1] ) + 1)
- (合并划分) f[i][j] = min(f[i][j], f[i][k] + f[k + 1][j]);
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int INF = 0x3f3f3f;
const int N = 120;
string s;
int f[N][N];
bool match(int l, int r) {
if(s[l] == '(' && s[r] == ')') return true;
if(s[l] == '[' && s[r] == ']') return true;
return false;
}
int main ( ) {
cin >> s;
int n = s.size();
for(int i = 0; i < n; i ++) {
f[i][i] = 1; // 初始化单个字母的区间
}
for(int len = 2; len <= n; len ++) {
for(int l = 0; l + len - 1 < n; l ++) {
int r = l + len - 1;
f[l][r] = INF; // 初始化区间
if(match(l, r)) f[l][r] = f[l + 1][r - 1]; // 如果匹配 则区间缩减 1
f[l][r] = min(f[l][r], min(f[l + 1][r], f[l][r - 1]) + 1); // 抉择 取最优方案
for(int k = l; k < r; k ++) {
// 抉择合并方案
f[l][r] = min(f[l][r], f[l][k] + f[k + 1][r]);
}
}
}
cout << f[0][n - 1] << endl;
return 0;
}
树形DP
没有上司的舞会 (树形dp)
- 思路
状态表示
状态计算:f是s字节的子结点
- 不选:
f[u][0] += max(f[j][0], f[j][1])
- 选:
f[u][1] += f[j][1]
- C++代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
#include <string>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
#define rep(i, a, n) for(int i = a; i <= n; i ++)
#define per(i, a, n) for(int i = n; i <= a; i --)
#define pb push_back
#include <stdlib.h> // atoi
#define debug cout<<"debug"<<"\n"
#define endl "\n"
const int INF = 0x3f3f3f3f;
const int mod=1e9+7;
const int N = 6010;
#define x first
#define y second
int n;
int happy[N];
int h[N], ne[N], idx, e[N];
int f[N][2];
bool has_father[N];
void add (int a, int b) {
// 把b 加到a
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u) {
f[u][1] = happy[u]; // 选了
for(int i = h[u]; i != -1; i = ne[i] ) {
// 遍历树
int j = e[i];
dfs(j); // 递归下一层
f[u][0] += max(f[j][0], f[j][1]); // 不选u 则可以选或者不选 取最优解即可
f[u][1] += f[j][0]; // 选u 则他的子节点都不选
}
}
void solve () {
scanf("%d", &n);
for(int i = 1; i <= n; i ++) {
scanf("%d", &happy[i]);
}
memset(h, -1, sizeof h);
for(int i = 0; i < n - 1; i ++) {
int a, b;
scanf("%d%d", &a, &b);
has_father[a] = true; // 说明a 有直接上司
add(b, a);
}
int root = 1; // 统计根节点个数
while(has_father[root]) {
root ++;
}
dfs(root);
printf("%d\n", max(f[root][0], f[root][1]));
}
int main(void){
// freopen("in.txt","r",stdin);
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
AcWing 1220. 生命之树 (树形dp)
#include<iostream>
#include<cstring>
#include<cstdio>
#include <limits.h>
#include<algorithm>
#include <string>
#include <vector>
#include <queue>
using namespace std;
typedef long long ll;
typedef double db;
typedef pair<int, int> PII;
#define rep(i, a, n) for(int i = a; i <= n; i ++)
#define per(i, a, n) for(int i = n; i <= a; i --)
#define pb push_back
#include <stdlib.h> // atoi
#define debug cout<<"debug"<<"\n"
#define endl "\n"
const int INF = 0x3f3f3f3f;
const int mod=1e9+7;
const int N = 100010;
const int M = N * 2;
#define x first
#define y second
// 上帝要在这棵树内选出一个非空节点集 S,使得对于 S 中的任意两个点 a,b,都存在一个点列 {a,v1,v2,…,vk,b} 使得这个点列中的每个点都是 S 里面的元素,且序列中相邻两个点间有一条边相连。
// 在这个前提下,上帝要使得 S 中的点所对应的整数的和尽量大。 === 实则就是求连通块的max
int n;
int w[N];
int h[N], ne[M], e[M], idx;
ll f[N];
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}
void dfs(int u, int father) {
// 记录父节点 防止往回走
f[u] = w[u];
for(int i = h[u]; i != -1; i = ne[i]) {
int j = e[i];
if(j != father) {
dfs(j, u);
f[u] += max(0ll, f[j]); // long long的0,和0比较一下,如果<=0没必要加上
}
}
}
void solve () {
cin >> n;
memset(h, -1, sizeof h);
for(int i = 1; i <= n; i ++) cin >> w[i];
for(int i = 0; i < n - 1; i ++) {
int a, b;
cin >> a >> b;
add (a, b) , add(b, a);
}
dfs(1, -1); // 跟节点没有父节点 所以第二个参数为 -1
ll res = f[1];
for(int i = 1; i <= n; i ++) res = max(res, f[i]);
cout << res << endl;
}
int main(void){
// freopen("in.txt","r",stdin);
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
int T = 1;
// cin >> T;
while(T --) solve();
return 0;
}
最大的和 (前后缀分解)
- 分别求前后缀连续序列的值 最后再比较即可
思路 :
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 50000 + 10;
const int INF = 1e9;
int n;
int w[N], f[N], g[N], h[N];
// g 最长连续前缀和 h : 最长连续后缀和
int main () {
int t;
scanf("%d", &t);
while(t --) {
n = 0;
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &w[i]);
f[0] = g[0] = -INF; // 边界
for (int i = 1; i <= n; i ++ ) {
f[i] = max(f[i - 1], 0) + w[i];
g[i] = max(g[i - 1], f[i]);
}
h[n + 1] = f[n + 1] = -INF;
for(int i = n; i ; i --) {
f[i] = max(f[i + 1], 0) + w[i];
h[i] = max(h[i + 1], f[i]);
}
int res = -INF;
for (int i = 1; i <= n; i ++ ) {
res = max(res, g[i] + h[i + 1]);
}
cout << res <<endl;
}
return 0;
}
同类习题 : acwing3652
- https://www.acwing.com/problem/content/description/484/
- https://www.luogu.com.cn/problem/P3143