动态规划
首要思路:分为状态表示和状态计算,而状态表示需要考虑到集合和属性。
1. 数字三角形模型:基于矩阵,有上下行的联系。
【方格取数】
从左上角走到右下角再返回,取过的数字变成0,求两趟数字之和最大。
f[2*N][N][N] 表示 第 k 步 横坐标走到 i1 另一个横坐标走到 i2 的最大价值
#include<bits/stdc++.h>
using namespace std;
const int N = 16;
int f[2*N][N][N]; // 按照步数 优化为 三维
int a[N][N];
int main()
{
int n; cin >> n;
int x,y,z;
while( cin >> x >> y >> z, x||y||z) a[x][y] = z;
for(int k = 2; k <= 2*n; k ++ )
{
for(int i1= 1; i1 <= n; i1 ++ )
{
for(int i2 = 1; i2 <= n; i2 ++ )
{
int j1 = k-i1, j2 = k-i2;
if(j1 >= 1&&j1 <= n&&j2 >= 1&&j2 <= n)
{
int &x = f[k][i1][i2];
int t = a[i1][j1];
if(i1!=i2) t += a[i2][j2];
x = max(x,f[k-1][i1-1][i2-1]+t);
x = max(x,f[k-1][i1-1][i2] +t);
x = max(x,f[k-1][i1][i2-1] +t);
x = max(x,f[k-1][i1][i2] +t);
}
}
}
}
cout<<f[2*n][n][n]<<endl;
return 0;
}
2. 最长上升子序列模型:有序的数列(升或降)
【最长公共上升子序列】
f[i][j] 表示 a[0~i] 和 b[0~j] 中以 b[j] 结尾的最长公共上升子序列的长度
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 3010;
int n;
int a[N], b[N];
int f[N][N];
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for (int i = 1; i <= n; i ++ ) scanf("%d", &b[i]);
for (int i = 1; i <= n; i ++ )
{
int maxv = 1;
for (int j = 1; j <= n; j ++ )
{
f[i][j] = f[i - 1][j];
if (a[i] == b[j]) f[i][j] = max(f[i][j], maxv);
if (a[i] > b[j]) maxv = max(maxv, f[i - 1][j] + 1);
}
}
int res = 0;
for (int i = 1; i <= n; i ++ ) res = max(res, f[n][i]);
printf("%d\n", res);
return 0;
}
3. 背包模型:
选与不选,基本状态表示:所有只从前 i 个物品中选,总体积不超过 j 的集合
属性有:Max Min Count ...
01背包:
一个物品只能用一次 f[i,j] = max(f[i-1,j],f[i-1,j-v]+w)
完全背包:遍历体积为 顺序 遍历
一个物品可以使用无限次 f[i,j] = max(f[i-1,j],f[i,j-v]+w)
多重背包:(朴素,二进制优化,单调队列优化)
一个物品可以使用 s[i] 次
for 物品
for 体积
for 决策
【混合背包】
s ==-1 01背包
s == 0 完全背包
s > 0 多重背包
#include<iostream>
using namespace std;
const int N = 1e3+6;
int f[N];
int main()
{
int n, m;
cin >> n >> m;
for(int i = 0; i < n; i ++ )
{
int v, w, s;
cin >> v >> w >> s;
if(s == 0) {
for(int j = v; j <= m; j ++ )
f[j] = max(f[j], f[j-v]+w);
}else {
if(s==-1) s = 1;
for(int k = 1; k <= s; k*= 2 )
{
for(int j = m; j <= m && k*v <= j; j -- )
f[j] = max(f[j], f[j-k*v]+k*w);
s-=k;
}
if(s)
{
for(int j = m; j >= v*s; j -- )
f[j] = max(f[j], f[j-s*v]+s*w);
}
}
}
cout<<f[m]<<endl;
return 0;
}
4. 状态机模型:理清每个状态 如何转移 画出图形
【股票买卖V】
f[i][j]
j==0 当前没有股票,且不处于冷冻期 (空仓)
j==1 当前有股票 (持仓)
j==2 当前没有股票,且处于冷冻期 (冷冻期)
#include <iostream>
#include <cstring>
using namespace std;
const int N = 1e5 + 10;
int n;
int w[N];
int f[N][3];
int main()
{
cin >> n;
for (int i = 1; i <= n; ++ i) cin >> w[i];
memset(f, -0x3f, sizeof f);
f[0][0] = 0;
for (int i = 1; i <= n; ++ i)
{
f[i][0] = max(f[i - 1][0], f[i - 1][2]);
f[i][1] = max(f[i - 1][1], f[i - 1][0] - w[i]);
f[i][2] = f[i - 1][1] + w[i];
}
cout << max(f[n][0], f[n][2]) << endl;
return 0;
}
5. 状态压缩DP: 用二进制来对数据进行压缩 用01来表示状态
check() 函数和 for 循环 很重要
【小国王】
在 n×n 的棋盘上放 k 个国王,国王可攻击相邻的 8 个格子,
求使它们无法互相攻击的方案总数。
f[i][k][j] 表示 到 第 i 行 已经摆放了 k 个国王 且状态为 j 的 方案数
#include<cstring>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
typedef long long LL;
const int N = 12, M = 1 << 10, K = 106;
int n,m;
vector<int> state; // 满足要求的行的情况
int cnt[M];
vector<int> head[M];
LL f[N][K][M];
bool check(int state)
{
for(int i = 0; i < n; i ++ )
if((state >> i & 1) && (state >> (i+1) & 1))
return false;
return true;
}
int count(int state)
{
int res = 0;
for(int i = 0; i < n; i ++ )
res += state >> i & 1;
return res;
}
int main()
{
cin >> n >> m;
for(int i = 0; i < 1 << n; i ++ )
if(check(i))
{
state.push_back(i);
cnt[i] = count(i);
}
for(int i = 0; i < state.size(); i ++ )
for(int j = 0; j < state.size(); j ++ )
{
int a = state[i], b = state[j];
if((a&b) == 0 && check(a|b))
head[i].push_back(j);
}
f[0][0][0] = 1;
for(int i = 1; i <= n+1; i ++ )
for(int j = 0; j <= m; j ++ )
for(int a = 0; a < state.size(); a ++ )
for(int b : head[a])
{
int c = cnt[state[a]];
if(j >= c)
f[i][j][a] += f[i-1][j-c][b];
}
cout<<f[n+1][m][0]<<endl;
return 0;
}
6. 区间DP:考虑一段区间的问题 对 len 进行最外层循环
【环形石子合并】
将环形 转换为 2倍线性
将累加看作前缀和 求前缀和 的 和 即为值
关键 ==> f[l][r] = min(f[l][r], f[l][k]+f[k+1][r]+s[r]-s[l-1]);
#include<bits/stdc++.h>
using namespace std;
const int N = 406;
int n;
int w[N],s[N];
int f[N][N],g[N][N];
int main()
{
cin >> n;
for(int i = 1; i <= n; i ++ )
{
cin >> w[i];
w[i+n] = w[i];
}
for(int i = 1; i <= 2*n; i ++ ) s[i] = s[i-1] + w[i];
memset(f,0x3f,sizeof f);
memset(g,-0x3f,sizeof g);
for(int len = 1; len <= n; len ++ )
{
for(int l = 1; l + len -1 <= 2*n; l ++ )
{
int r = l + len - 1;
if( l == r) f[l][r] = g[l][r] = 0;
else
{
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]);
g[l][r] = max(g[l][r], g[l][k]+g[k+1][r]+s[r]-s[l-1]);
}
}
}
}
int ans1 = 0x3f3f3f3f, ans2 = -0x3f3f3f3f;
for(int i = 1; i <= n; i ++ )
{
ans1 = min(ans1,f[i][i+n-1]);
ans2 = max(ans2,g[i][i+n-1]);
}
cout<<ans1<<endl<<ans2<<endl;
return 0;
}
7. 树形DP: 以树的数据结构 来 构成问题
其实就是 换一种数据的存储方式 本质上 跟 线性也无差别
【二叉苹果树】
求 剩下 m 个树枝 收获苹果的最大价值
相当于 求 m 条边的一个连通块且1为根节点 的最大价值
f[i][j] 表示 以 i 为根节点 剩余 j 条边的最大价值
#include <iostream>
#include <cstring>
using namespace std;
const int N = 110, M = N << 1;
int n, m;
int h[N], e[M], w[M], ne[M], idx;
int f[N][N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
void dfs(int u, int father)
{
for (int i = h[u]; ~i; i = ne[i])
{
int ver = e[i];
if (ver == father) continue;
dfs(ver, u);
for (int j = m; j >= 0; j -- )
for (int k = 0; k <= j - 1; k ++ ) //枚举体积预留一条连向父节点的边
f[u][j] = max(f[u][j], f[u][j - k - 1] + f[ver][k] + w[i]);
}
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d%d", &n, &m);
for (int i = 1; i < n; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c), add(b, a, c);
}
dfs(1, -1);
printf("%d\n", f[1][m]);
return 0;
}
【树的最长路径】
树的遍历 参考以下 dfs 方式
int dfs(int u,int father)
{
int dist = 0;
int d1 = 0, d2 = 0;
for(int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if(j == father) continue;
int d = dfs(j,u) + w[i];
dist = max(dist, d);
if(d >= d1) d2 = d1, d1 = d;
else if(d > d2) d2 = d;
}
ans = max(ans,d1+d2);
return dist;
}
8. 数位DP:求一个区间内满足条件的数字有多少个
【度的量化】
求 在 [l,r] 范围内 b进制数中含有k个1的数的个数
#include<bits/stdc++.h>
using namespace std;
const int N = 36;
int f[N][N],l,r,k,b;
void init()
{
for(int i = 0; i < N; i ++ )
for(int j = 0; j <= i; j ++ )
if(!j) f[i][j] = 1;
else f[i][j] = f[i-1][j] + f[i-1][j-1];
}
int dp(int n)
{
int res = 0;
int last = 0;
if(!n) return 0;
vector<int> nums;
while(n) nums.push_back(n%b), n/=b;
for(int i = nums.size()-1; i >= 0; i -- )
{
int x = nums[i];
if(x)
{
res += f[i][k-last];
if(x > 1)
{
if(k-last-1 >= 0) res += f[i][k-last-1];
break;
}
else
{
last ++;
if(last > k) break;
}
}
if( !i && last == k) res++;
}
return res;
}
int main()
{
init();
cin >> l >> r >> k >> b;
cout<<dp(r) - dp(l-1)<<endl;
return 0;
}
9. 单调队列优化DP:
【最大子序列和】
长度为 n 的序列中 m 长度子序列的最大值
#include<bits/stdc++.h>
using namespace std;
const int N = 3e5+6, INF = 0x3f3f3f3f;
int s[N];
int n,m;
int q[N],hh,tt;
int main()
{
cin >> n >> m;
for(int i = 1; i <= n; i ++ ) scanf("%d",&s[i]), s[i]+=s[i-1];
int res = -INF;
for(int i = 1; i <= n; i ++ )
{
if(q[hh] < i-m) hh++;
res = max(res, s[i]-s[q[hh]]);
while(hh <= tt && s[q[tt]] >= s[i]) tt--;
q[++tt] = i;
}
cout<<res<<endl;
return 0;
}
10. 斜率优化DP