DP总结全(从基础到提高)

目录

前言:

  1. 感觉没什么好总结的emm。从来没有系统学过,但是很多东西感觉都还是会的。
  2. 之前写的博客DP基础知识总结(富文本格式,已删除)转化为这个博客(markdown格式)

刷过的题/专题:

1.“kuangbin带你飞”专题计划——专题十二:基础DP1

  1. 链接“kuangbin带你飞”专题计划——专题十二:基础DP1

2.“kuangbin带你飞”专题计划——专题十五 数位DP

  1. 链接“kuangbin带你飞”专题计划——专题十五 数位DP

3.

基础知识:

一、背包DP

参考资料:

  1. 【笔记】背包九讲-整合版
  2. oi-wiki-背包 DP
  3. 繁凡さん-【算法】动态规划+“背包九讲”原理超详细讲解+常见dp问题(9种)总结
  4. 建议学习:oi-wiki-背包 DP

1.01背包

  1. 题目:给定物品个数n,背包容量v,每个物品都有一个体积c和价值w,要求向背包中装物品使得总价值最高。
  2. 题解 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[i1][j],dp[i1][jc]+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[jc]+w)
    • 以上式子,左边的dp[j]表示的是考虑前 i 个物品的情况,右边的是考虑前 i-1 个物品的情况。如果想要一维实现,那加入第 i 件物品的时候 j 就要从后往前枚举讨论。
    • 如果 j 正向枚举就变成可以多次取该物品了——完全背包
  3. 代码
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[jc[i]]已经有值(比如初始化为-1,dp[0]=0,那么如果 d p [ j − c [ i ] ] = − 1 dp[j-c[i]]=-1 dp[jc[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.完全背包

  1. 题目:与01背包不同的是,每个物品可以取无数次
  2. 题解:01背包代码中,j 由前向后枚举即可
  3. 代码
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.多重背包

  1. 题目:与01背包和完全背包不同的是,每个物品可以取ki次
  2. 题解:将ki二进制处理一下(比如10=1+2+4+3,19=1+2+4+8+4,14=1+2+4+7),然后就转化为了裸的01背包。
  3. 拆分的时候注意:从小到大 ( 2 0 , 2 1 , 2 2 , . . . . . ) (2^0,2^1,2^2,.....) 20,21,22,.....拆分,知道不能拆分,剩下的也要处理。另外,别忘了容量和价值都要变成拆分的个数倍。
    代码:略(有的东西,知道思路就ok)

4.混合背包

1. 讨论、混合一下其他背包就ok

5.二维费用背包

  1. 题目:与01背包不同的是,选一种物品会消耗两种价值(上面解释的只消耗容量c)
  2. 题解:多一重循环罢了
  3. 例题P1855 榨取kkksc03
  4. 代码:略

6.分组背包

  1. 题目:与01背包不同的是,有一些物品我们归为一组,组内的物品不能同时选,会发生冲突
  2. 题解:对每一组进行一次01背包即可(说的挺简单,之前写过早忘了emm,现在也还没完全搞懂,等遇到题目再说)
  3. 例题P1757 通天之分组背包

7.泛化物品的背包

在这里插入图片描述

8.有依赖的背包

  1. 例题P1064 [NOIP2006 提高组] 金明的预算方案
    在这里插入图片描述

9.杂项

在这里插入图片描述

状压DP

题目1:9*9格子里面有k个国王,求互不攻击的种类数

  1. 传送门P1896 [SCOI2005]互不侵犯
  2. 题意:在 n ∗ n n*n nn的格子里面有 k k k个国王,求他们互不攻击的种类数。
    1. 互不攻击:一个国王的上、下、左、右、上左、下左、上右、下右都没有国王。
    2. 1 ≤ n ≤ 9 , 0 ≤ k ≤ n ∗ n 1\le n\le 9,0\le k\le n*n 1n9,0knn
  3. 题解:状压DP,dp[i][j][k]标识第 i i i行、状态为 j j j、国王总数为 k k k的种类数。
    1. 更新的时候从上层往下层更新,满足本层不攻击已经与上层不攻击就可以转移。
  4. 代码
#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个偶像团体站成一排,求最少出列人数(出列之后回到剩下的空位中)使来自一个偶像团体的人排在一起

  1. 传送门P3694 邦邦的大合唱站队

  2. 题意 n n n个偶像团体任意排成一排,他们来自 m m m个偶像团体,其中一部分出列,其他人不动,然后出列的人回到原来的空位(不一定是自己原来占的位置,事实上一定不是自己原来占的位置)。求最少出列人数。

    1. 1 ≤ n ≤ 1 e 5 , 1 ≤ m ≤ 20 1\le n\le 1e5,1\le m\le 20 1n1e5,1m20
    2. m m m个偶像团体每个偶像团体至少有一个人 。
    3. 帮助理解
      在这里插入图片描述
  3. 题解:状压DP,dp[i]表示状态 i i i需要出去的人的最少位数。状态1101表示1,3,4团体在最前面(注意,关键在于1,3,4不一定是按顺序,只要在一堆就ok)。

  4. 代码

#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.将一个数组变成 严格&不严格,不递增&不递减 的数组的最小代价

  1. 例题Making the Grade POJ - 3666 (将一个数组变成 严格&不严格,不递增&不递减 的数组的最小代价)
  2. 题意:给定一个长度为2000的数组a,ai的范围为 0 − 1 e 9 0-1e9 01e9,改变ai的代价为改变值的绝对值,问将数组a变为不严格单调数组的最小代价
  3. 题解(只解释不严格单增,单减类似):mp数组离散化(mp[j]为第j大的数),然后dp[i][j]表示前 i 个数最大值为 mp[j] 的最小代价,状态转移方程为
    • 如果要求将a变成严格单调数组的最小代价:只需要最开始把 ai:=ai-i 即可。
  4. 代码
#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.矩形中求最大的对称正方形

  1. 例题Phalanx HDU - 2859 (矩形中求最大的对称正方形)(具体的看链接内容,以下很简略)
  2. 题意:给定一个n*n正方形(0<n<=1000),求最大的对称正方形,对称线为从左下角到右上角的线。
  3. 题解:从右上角开始递推求dp[i][j]
  4. 代码
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.最长上升子序列

  1. 介绍:有两种写法,一种O(n^2),另一种O(nlogn)(实际上就是加个upper_bound)。
  2. 注意,upper_bound(a,a+1+n,x)-a。如果数组a[0]~a[n]都没有比x大的数,那就返回n+1(STL中返回的是end()…)
  3. 题目
  4. 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;
        }
    }
}
  1. 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.最长公共子序列

  1. 题目:Common Subsequence POJ - 1458
  2. 题意:给定两个字符串,求出最长公共子序列
  3. 题解:看代码,自己思考。注意,代码中的dp数组可以优化为一维(自己思考)
  4. 代码
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

  1. 题目:Treats for the Cows POJ - 3186 (双端队列有权取数+区间DP)
  2. 题意:一个长度小于等于2000的数组a,每次可以再头部或尾部取数,ai为第k个取的数,则需要花费k*ai的代价,求取完数的最大代价。
  3. 题解:dp[i][j]表示前面取了a[0-i],后面取了a[j-n+1]的最大代价,很明显,dp[i][j]只与dp[i-1][j]或者dp[i][j+1]有关。
  4. 代码
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
    • 首先思考是不是只需要一步就能够转化得到,实现不行才思考是不是需要多步。(一步即指由已得出的一个或两个最优子结构决策得出,多步指由很多最优子结构决策得出)

随便刷题

1.常识级DP:删除数a[i],还是删除a[i]前面的数?(cf*2000)

  1. E. Fixed Points
  2. 题意:给定n,k( 1 ≤ k ≤ n ≤ 2000 1\le k\le n\le 2000 1kn2000), n 表示数组 a 的长度( a i ≤ n a_i\le n ain)。
    1. 求删除最少的数使满足 a i = i a_i=i ai=i的数最多,删除一个数的时候,后面的数要往前走。​
    2. 输出:一个正数,表示最小值,如果一个数都不用删除,那就输出-1。
  3. 题解:见标题,dp[i][j]表示前i个数删除j个数时的“最大满足ai==i的个数”
  4. 代码
#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. F. Telepanting
  2. 代码
/*
首先要清楚的几个点:
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时右端点离左端点的距离)(这题难的就是找到正确的、容易转移的状态!!!)

  1. 传送门G. Minimal Coverage
  2. 代码
/*
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

*/
  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值