前言:
- 感觉没什么好总结的emm。从来没有系统学过,但是很多东西感觉都还是会的。
- 之前写的博客DP基础知识总结(富文本格式,已删除)转化为这个博客(markdown格式)
刷过的题/专题:
1.“kuangbin带你飞”专题计划——专题十二:基础DP1
2.“kuangbin带你飞”专题计划——专题十五 数位DP
3.
基础知识:
一、背包DP
参考资料:
- 【笔记】背包九讲-整合版
- oi-wiki-背包 DP
- 繁凡さん-【算法】动态规划+“背包九讲”原理超详细讲解+常见dp问题(9种)总结
- 建议学习:oi-wiki-背包 DP
1.01背包
- 题目:给定物品个数n,背包容量v,每个物品都有一个体积c和价值w,要求向背包中装物品使得总价值最高。
- 题解:
d
p
[
i
]
[
j
]
=
max
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
c
]
+
w
)
dp[i][j]=\max(dp[i-1][j],dp[i-1][j-c]+w)
dp[i][j]=max(dp[i−1][j],dp[i−1][j−c]+w),其中
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示在前 i 个物品中体积为 j 的最大价值。max中的式子分别表示不取,取第 i 件物品。
- 优化空间复杂度: d p [ j ] = max ( d p [ j ] , d p [ j − c ] + w ) dp[j]=\max(dp[j],dp[j-c]+w) dp[j]=max(dp[j],dp[j−c]+w)
- 以上式子,左边的dp[j]表示的是考虑前 i 个物品的情况,右边的是考虑前 i-1 个物品的情况。如果想要一维实现,那加入第 i 件物品的时候 j 就要从后往前枚举讨论。
- 如果 j 正向枚举就变成可以多次取该物品了——完全背包
- 代码:
for (int i = 1; i <= n; i++) {
for (int j = m; j >= c[i]; j--) { // m表示最大容量
dp[j] = max(dp[j], dp[j - c[i]] + w[j]); //用j之前的j-c[i]更新
}
}
- 注意:这里dp[i]表示的是容量为 i 时能装的物品的最大价值,但不是容量恰好为 i 时能装的物品的最大价值。如果要恰好的话,就需要更新时的 d p [ j − c [ i ] ] dp[j-c[i]] dp[j−c[i]]已经有值(比如初始化为-1,dp[0]=0,那么如果 d p [ j − c [ i ] ] = − 1 dp[j-c[i]]=-1 dp[j−c[i]]=−1就不能更新)
memset(dp, -1, sizeof(dp));
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = m; j >= c[i]; j--) { // m表示最大容量
if (dp[j - c[i]] == -1) continue;
dp[j] = max(dp[j],
dp[j - c[i]] + w[j]); // dp[j]表示恰好容量为j时的最大价值
}
}
- ps:其他背包也是这样,有这个意识就ok了。——注意是不超过容量就ok还是必须要刚好那么多容量。
2.完全背包
- 题目:与01背包不同的是,每个物品可以取无数次
- 题解:01背包代码中,j 由前向后枚举即可
- 代码:
for (int i = 1; i <= n; i++) {
for (int j = c[i]; j <= m; j++) {
dp[j] = max(dp[j], dp[j - c[i]] + w[j]); //用j之前的j-c[i]更新
}
}
3.多重背包
- 题目:与01背包和完全背包不同的是,每个物品可以取ki次
- 题解:将ki二进制处理一下(比如10=1+2+4+3,19=1+2+4+8+4,14=1+2+4+7),然后就转化为了裸的01背包。
- 拆分的时候注意:从小到大
(
2
0
,
2
1
,
2
2
,
.
.
.
.
.
)
(2^0,2^1,2^2,.....)
(20,21,22,.....)拆分,知道不能拆分,剩下的也要处理。另外,别忘了容量和价值都要变成拆分的个数倍。
代码:略(有的东西,知道思路就ok)
4.混合背包
5.二维费用背包
- 题目:与01背包不同的是,选一种物品会消耗两种价值(上面解释的只消耗容量c)
- 题解:多一重循环罢了
- 例题: P1855 榨取kkksc03
- 代码:略
6.分组背包
- 题目:与01背包不同的是,有一些物品我们归为一组,组内的物品不能同时选,会发生冲突
- 题解:对每一组进行一次01背包即可(说的挺简单,之前写过早忘了emm,现在也还没完全搞懂,等遇到题目再说)
- 例题:P1757 通天之分组背包
7.泛化物品的背包
8.有依赖的背包
9.杂项
状压DP
题目1:9*9格子里面有k个国王,求互不攻击的种类数
- 传送门:P1896 [SCOI2005]互不侵犯
- 题意:在
n
∗
n
n*n
n∗n的格子里面有
k
k
k个国王,求他们互不攻击的种类数。
- 互不攻击:一个国王的上、下、左、右、上左、下左、上右、下右都没有国王。
- 1 ≤ n ≤ 9 , 0 ≤ k ≤ n ∗ n 1\le n\le 9,0\le k\le n*n 1≤n≤9,0≤k≤n∗n
- 题解:状压DP,
dp[i][j][k]
标识第 i i i行、状态为 j j j、国王总数为 k k k的种类数。- 更新的时候从上层往下层更新,满足本层不攻击已经与上层不攻击就可以转移。
- 代码:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1024 + 10;
int n, k;
int num[N], dp[9 + 5][N][81 + 5];
vector<int> v;
void init(int t) {
for (int i = 0; i <= t; i++) {
int x = i;
if (x & (x << 1LL)) continue;
while (x) {
if (x % 2 == 1) num[i]++;
x /= 2;
}
v.push_back(i);
}
// for (int i = 0; i <= t; i++) cout << ":::" << i << " " << num[i] << endl;
}
signed main() {
scanf("%lld%lld", &n, &k);
int t = (1LL << n) - 1;
init(t);
//先学习郑星宇,把最简单的写法写出来
for (int i = 0; i <= t; i++) dp[1][i][num[i]]++; //数量
for (int i = 2; i <= n; i++) { // i行
for (auto j : v) { //状态j
for (auto k1 : v) { //上一层状态
for (int k2 = 0; k2 <= k; k2++) { //上一层数量
if ((j & k1) || ((j << 1LL) & k1) || (j & (k1 << 1LL)))
continue;
// cout << ">>>" << j << " " << k1 << endl;
if (k2 + num[j] <= k)
dp[i][j][k2 + num[j]] += dp[i - 1][k1][k2];
}
}
}
}
int ans = 0;
for (int j = 0; j <= t; j++) ans += dp[n][j][k];
printf("%lld\n", ans);
return 0;
}
题目2:n个人来自m个偶像团体站成一排,求最少出列人数(出列之后回到剩下的空位中)使来自一个偶像团体的人排在一起
-
传送门:P3694 邦邦的大合唱站队
-
题意: n n n个偶像团体任意排成一排,他们来自 m m m个偶像团体,其中一部分出列,其他人不动,然后出列的人回到原来的空位(不一定是自己原来占的位置,事实上一定不是自己原来占的位置)。求最少出列人数。
- 1 ≤ n ≤ 1 e 5 , 1 ≤ m ≤ 20 1\le n\le 1e5,1\le m\le 20 1≤n≤1e5,1≤m≤20
- m m m个偶像团体每个偶像团体至少有一个人 。
- 帮助理解:
-
题解:状压DP,
dp[i]
表示状态 i i i需要出去的人的最少位数。状态1101
表示1,3,4团体在最前面(注意,关键在于1,3,4不一定是按顺序,只要在一堆就ok)。 -
代码:
#include <bits/stdc++.h>
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
// const int N = 1e6 + 10;//(1e6+10)<(1<<20)
const int N = (1 << 20) + 10;
int n, m, a[N];
int sum[N][20 + 10];
int dp
[N]; // dp[i]表示状态为i的时候需要删除的最少的数。其中状态1101表示前面4组为1,3,4的时候(注意是1,3,4中的任意顺序!!!)
signed main() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= m; j++) sum[i][j] = sum[i - 1][j];
sum[i][a[i]]++;
}
// for (int i = 1; i <= m; i++) cout << ":::" << i << " " << sum[n][i] <<
// endl;
int t = (1 << m) - 1;
for (int i = 1; i <= t; i++) dp[i] = 1e9; // dp[0]=0
dp[0] = 0;
for (int i = 0; i <= t; i++) {
int cnt = 0, l, r;
for (int j = 0; j < m; j++)
if ((i >> j) & 1) cnt += sum[n][j + 1]; //占了前面cnt位
for (int j = 0; j < m; j++) {
if ((i >> j) & 1) continue;
l = cnt + 1, r = cnt + sum[n][j + 1]; //转移,多一位
dp[i | (1 << j)] =
min(dp[i | (1 << j)], dp[i] + sum[n][j + 1] -
(sum[r][j + 1] - sum[l - 1][j + 1]));
// sum[n][j+1]也表示区间长度
}
}
// for (int i = 0; i <= t; i++) cout << ">>>" << dp[i] << endl;
printf("%d\n", dp[t]);
return 0;
}
一些DP经典经典例题
1.将一个数组变成 严格&不严格,不递增&不递减 的数组的最小代价
- 例题:Making the Grade POJ - 3666 (将一个数组变成 严格&不严格,不递增&不递减 的数组的最小代价)
- 题意:给定一个长度为2000的数组a,ai的范围为 0 − 1 e 9 0-1e9 0−1e9,改变ai的代价为改变值的绝对值,问将数组a变为不严格单调数组的最小代价
- 题解(只解释不严格单增,单减类似):mp数组离散化(mp[j]为第j大的数),然后dp[i][j]表示前 i 个数最大值为 mp[j] 的最小代价,状态转移方程为
- 如果要求将a变成严格单调数组的最小代价:只需要最开始把 ai:=ai-i 即可。
- 代码:
#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <map>
#include <string>
#define int long long
using namespace std;
const int N = 2e3 + 10;
const int INF = 1e18;
int n, a[N], b[N];
int cnt;
int mp[N];
int dp[N][N];
signed main() {
cin >> n;
int i, j, mi, ans = INF;
for (i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
sort(b + 1, b + 1 + n);
b[0] = -1; //如果b[1]=0,那么mp[1]=0
for (i = 1; i <= n; i++) {
if (b[i] == b[i - 1])
continue;
else
mp[++cnt] = b[i];
}
// dp[i][j]表示前i个数中,最大值刚好为mp[j]的最小操作数——单增
for (j = 1; j <= cnt; j++) dp[1][j] = abs(mp[j] - a[1]);
for (i = 2; i <= n; i++) {
mi = INF;
for (j = 1; j <= cnt; j++) {
mi = min(mi, dp[i - 1][j]);
dp[i][j] = mi + abs(a[i] - mp[j]);
}
}
for (j = 1; j <= cnt; j++) ans = min(ans, dp[n][j]);
// cout << ans << endl;
// return 0;
//单减:i~n个数最小值为mp[j]
for (j = cnt; j >= 1; j--) dp[n][j] = abs(mp[j] - a[n]);
for (i = n - 1; i >= 1; i--) {
mi = INF;
for (j = cnt; j >= 1; j--) {
mi = min(mi, dp[i + 1][j]);
dp[i][j] = mi + abs(a[i] - mp[j]);
}
}
for (j = cnt; j >= 1; j--) ans = min(ans, dp[1][j]);
cout << ans << endl;
return 0;
}
/*
input:::
7
1 3 2 4 5 3 9
output:::
3
*/
2.矩形中求最大的对称正方形
- 例题:Phalanx HDU - 2859 (矩形中求最大的对称正方形)(具体的看链接内容,以下很简略)
- 题意:给定一个n*n正方形(0<n<=1000),求最大的对称正方形,对称线为从左下角到右上角的线。
- 题解:从右上角开始递推求dp[i][j]
- 代码:
for (i = 1; i <= n; i++)
for (j = 1; j <= n; j++) dp[i][j] = 1;
for (i = 1; i <= n; i++) {
for (j = n; j >= 1; j--) {
int ni = i, nj = j;
ni--, nj++;
while (ni >= 1 && nj <= n && dp[i][j] <= dp[i - 1][j + 1] &&
c[ni][j] == c[i][nj]) {
ni--, nj++;
dp[i][j]++;
}
ans = max(ans, dp[i][j]);
}
}
3.最长上升子序列
- 介绍:有两种写法,一种O(n^2),另一种O(nlogn)(实际上就是加个upper_bound)。
- 注意,upper_bound(a,a+1+n,x)-a。如果数组a[0]~a[n]都没有比x大的数,那就返回n+1(STL中返回的是end()…)
- 题目:
- O(n^2)代码:
for (i = 1; i <= n; i++) {
for (j = cnt; j >= 0; j--) {
if (a[i] > dp[j] || j == 0) {
dp[j + 1] = a[i];
cnt = max(cnt, j + 1);
break;
}
}
}
- O(nlogn)代码:
dp[0] = 0, cnt = 0;
for (i = 1; i <= n; i++) {
int pos = upper_bound(dp, dp + 1 + cnt, a[i]) - dp;
// upper_bound,lower_bound:如果找不到比x大的数就返回v.end()
dp[pos] = a[i], cnt = max(cnt, pos);
}
4.最长公共子序列
- 题目:Common Subsequence POJ - 1458
- 题意:给定两个字符串,求出最长公共子序列
- 题解:看代码,自己思考。注意,代码中的dp数组可以优化为一维(自己思考)
- 代码:
int n, m;
char a[N], b[N];
int dp[N][N];
signed main() {
while (scanf("%s%s", a + 1, b + 1) != EOF) {
n = strlen(a + 1), m = strlen(b + 1);
int i, j;
for (i = 1; i <= n; i++) {
for (j = 1; 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][m] << endl;
}
return 0;
}
5.双端队列带权取数+区间DP
- 题目:Treats for the Cows POJ - 3186 (双端队列有权取数+区间DP)
- 题意:一个长度小于等于2000的数组a,每次可以再头部或尾部取数,ai为第k个取的数,则需要花费k*ai的代价,求取完数的最大代价。
- 题解:dp[i][j]表示前面取了a[0-i],后面取了a[j-n+1]的最大代价,很明显,dp[i][j]只与dp[i-1][j]或者dp[i][j+1]有关。
- 代码:
int n, a[N];
int dp[N][N];
signed main() {
cin >> n;
int i, j;
for (i = 1; i <= n; i++) cin >> a[i];
for (i = 1; i <= n; i++) dp[i][n + 1] = dp[i - 1][n + 1] + i * a[i];
for (j = n; j >= 1; j--) dp[0][j] = dp[0][j + 1] + (n - j + 1) * a[j];
for (i = 1; i <= n; i++) {
for (j = n; j > i; j--) {
int k = i + (n - j + 1);
dp[i][j] = max(dp[i - 1][j] + k * a[i], dp[i][j + 1] + k * a[j]);
}
}
int ans = 0;
for (i = 0; i <= n; i++) ans = max(ans, dp[i][i + 1]);
cout << ans << endl;
return 0;
}
/*
input:::
5
1 3 1 5 2
output:::
43
*/
- 拓展:
- 不带权就相当于权全部为1
- 首先思考是不是只需要一步就能够转化得到,实现不行才思考是不是需要多步。(一步即指由已得出的一个或两个最优子结构决策得出,多步指由很多最优子结构决策得出)
随便刷题
1.常识级DP:删除数a[i],还是删除a[i]前面的数?(cf*2000)
- E. Fixed Points
- 题意:给定n,k(
1
≤
k
≤
n
≤
2000
1\le k\le n\le 2000
1≤k≤n≤2000), n 表示数组 a 的长度(
a
i
≤
n
a_i\le n
ai≤n)。
- 求删除最少的数使满足 a i = i a_i=i ai=i的数最多,删除一个数的时候,后面的数要往前走。
- 输出:一个正数,表示最小值,如果一个数都不用删除,那就输出-1。
- 题解:见标题,
dp[i][j]表示前i个数删除j个数时的“最大满足ai==i的个数”
- 代码:
#include <bits/stdc++.h>
#define dbg(x) cout << #x << "===" << x << endl
using namespace std;
const int N = 2e3 + 5;
int n, a[N], k;
int dp[N][N];
signed main() {
int T = 1;
cin >> T;
while (T--) {
cin >> n >> k;
for (int i = 1; i <= n; i++) {
cin >> a[i];
for (int j = 0; j <= i; j++) dp[i][j] = 0;
}
int ans = 1e9;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= i; j++) {
//删去a[i]/不删去a[i]
if (j == 0)
dp[i][j] = dp[i - 1][j] + (a[i] == i);
else
dp[i][j] =
max(dp[i - 1][j - 1], dp[i - 1][j] + (a[i] == i - j));
if (dp[i][j] >= k) ans = min(ans, j); //达到 k 的最小 j
}
}
if (ans == 1e9) ans = -1;
cout << ans << endl;
}
return 0;
}
2.DP好题:给一些传送带回从右边传到左边,问多少步能从0到达x[n]+1? (最优子结构的应用,只考虑从i回到i的多余消耗,不考虑具体过程)(cf*2200)
/*
首先要清楚的几个点:
1. 到达i的时候,前面所有的传送带都变成了可传送状态。
然后动态规划:
1. 定义dp[i]表示从i回到i的多余消耗
2. 然后我们只需要在s[i]==1的时候+dp[i]就ok了
至于怎么求dp[i]:
1. y[j]到x[i]与原题一样,“只需要在s[i]==1的时候+dp[i]就ok了” ————最优子结构
2. 一步一步来,每一步都会让你有收获,即使最多也不能独立想完所有的步骤。
*/
#include <bits/stdc++.h>
// #define int long long
// #define ll long long
using namespace std;
const int N = 2e5 + 5;
const int mod = 998244353;
int n, x[N], y[N], s[N];
int dp[N], sum[N];
signed main() {
cin >> n;
int ans = 0;
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &x[i], &y[i], &s[i]);
int p = upper_bound(x + 1, x + 1 + i, y[i]) - x;
// for (int j = p; j < i; j++) dp[i] = (dp[i] + dp[j]) % mod;
if (i > p) dp[i] = ((sum[i - 1] - sum[p - 1]) % mod + mod) % mod;
dp[i] = (dp[i] + (x[i] - y[i])) % mod;
if (s[i]) ans = (ans + dp[i]) % mod;
sum[i] = (sum[i - 1] + dp[i]) % mod;
}
ans = (ans + x[n] + 1) % mod;
// cout << ">>>>";
cout << ans << endl;
return 0;
}
/*
4
3 2 0
6 5 1
7 4 0
8 1 1
23
*/
3.有难度cf*2200:给定不超过10000条长度不超过1000线段,每条线段的起点应该为上一条线段的终点,求所有线段覆盖的范围的最小值?(找状态&思考怎么转移状态&这题状态可以为(i,j),dp[i][j]表示前i条线段离左端点距离为j时右端点离左端点的距离)(这题难的就是找到正确的、容易转移的状态!!!)
- 传送门:G. Minimal Coverage
- 代码:
/*
1. 如果一开始思路就错了,那是很浪费时间的。
1. 这题以我总是把问题想简单的习惯,如果不知道时*2200的题的话,很有可能会把它想成贪心,然后一直wa下去
*/
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define dbg(x) cout<<#x<<"==="<<x<<endl
using namespace std;
template<class T>
void read(T &x) {
T res=0,f=1;
char c=getchar();
while(!isdigit(c)) {
if(c=='-') f=-1;
c=getchar();
}
while(isdigit(c)) res=(res<<3)+(res<<1)+(c-'0'),c=getchar();
x=res*f;
}
const int N=1e4+5;
const int inf=1e9;
int n,a[N];
int dp[N][2005];
void init() {
// fill(dp,dp+1+n*2000,inf);
for(int i=0; i<=n; i++)
// for(int j=0; j<=2000; j++) dp[i][j]=inf;
fill(dp[i],dp[i]+1+2000,inf);//fill函数的使用
dp[0][0]=0;
}
signed main() {
int T;
read(T);
while(T--) {
read(n);
init();
for(int i=1; i<=n; i++) read(a[i]);
int x;
for(int i=0; i<n; i++) {
x=a[i+1];
for(int j=0; j<=2000; j++) {
if(dp[i][j]==inf) continue;
if(j+x<=2000) dp[i+1][j+x]=min(dp[i+1][j+x],max(dp[i][j],j+x));
if(j-x>=0) dp[i+1][j-x]=min(dp[i+1][j-x],dp[i][j]);
else dp[i+1][0]=min(dp[i+1][0],dp[i][j]+(x-j));
}
}
// for(int i=0; i<=n; i++) {
// dbg(i);
// for(int j=0; j<=20; j++) {
// printf("%4d",dp[i][j]);
// }
// cout<<endl;
// }
// cout<<">>>>>>>>>>";
int ans=inf;
for(int j=0; j<=2000; j++) {
// if(dp[n][j]==-1) continue;
ans=min(ans,dp[n][j]);
}
printf("%d\n",ans);
}
return 0;
}
/*
6
2
1 3
3
1 2 3
4
6 2 3 9
4
6 8 4 5
7
1 2 4 6 7 7 3
8
8 6 5 1 2 2 3 6
*/