动态规划详解

来更新一下,既然想写一篇动态规划,那么怎么能少了01背包。
主要是我 01背包顺推逆推每次都要重新理解一次

先奉上题目:
https://www.acwing.com/problem/content/2/
我们设 f i , j f_{i,j} fi,j为前 i i i个物品容量为 j j j能收获的最大贡献。那么 f i , j = max ⁡ ( f i − 1 , j , f i , − 1 , j − v i + w i ) f_{i,j}=\max(f_{i-1,j},f_{i,-1,j-v_i}+w_i) fi,j=max(fi1,j,fi,1,jvi+wi)
这个式子表示对于第 i i i 件物品,选择它或不选它能产生的最大价值。
它的状态完全由前 i − 1 i-1 i1 个物品的状态转移而来。

#include<bits/stdc++.h>
using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn][maxn],w[maxn],v[maxn];
signed main(){
    int n,m;
    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 = 1;j <= m;j++){
            if(j>=v[i]){
                dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - v[i]] + w[i]);
            }
            else dp[i][j] = dp[i - 1][j];
        }
    }
    cout<< dp[n][m] <<endl;
   return 0;
}

这里的时间无法再优化,但空间复杂度还可以再优化,我们注意到,当前的状态(假设为 i i i)只与 i − 1 i-1 i1的状态有关。那么我们另设一个数组存 i − 1 i - 1 i1的状态,那么就可以将空间复杂度由 O ( n m ) O(nm) O(nm)降至 O ( 2 m ) O(2m) O(2m) f [ j ] = max ⁡ ( s t a [ j ] , s t a [ j − v [ i ] ] + w [ i ] ) f[j]=\max(sta[j],sta[j-v[i]]+w[i]) f[j]=max(sta[j],sta[jv[i]]+w[i])

#include<bits/stdc++.h>

using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn], w[maxn], v[maxn], sta[maxn];

signed main() {
    int n, m;
    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 = 1; j <= m; j++) {
            if (j >= v[i]) {
                dp[j] = max(sta[j], sta[j - v[i]] + w[i]);
            } else dp[j] = sta[j];
        }
        for (int j = 1; j <= m; j++)
            sta[j] = dp[j];
    }
    cout << dp[m] << endl;
    return 0;
}

很多初学者不明白01背包顺着推和逆着推的到底是什么逻辑。
现在我们再来优化一下上面的代码,来看看滚动数组是怎么逆推实现的。
上面我们要用 s t a sta sta 数组来记录前一个状态,现在,来试试不用 s t a sta sta 来记录状态。
我们只用 D P [ V ] DP[V] DP[V]来更新答案。
想想为什么我们要用 s t a sta sta 来记录前一个状态,因为对于现在的 d p [ j ] dp[j] dp[j],我们是对第 i i i 件物品选择或不选,如果顺推那就是: d p [ j ] = max ⁡ ( d p [ j ] , d p [ j − v [ i ] ] + w [ i ] ) dp[j]=\max(dp[j],dp[j-v[i]]+w[i]) dp[j]=max(dp[j],dp[jv[i]]+w[i])这个式子里, j j j 之前的状态已经讨论过 i i i 是否选取,也就是说,很有可能我当前选了 i i i,但是之前的 d p [ j − v [ i ] ] dp[j-v[i]] dp[jv[i]]也选过 i i i,造成了重复计数。
面对这种情况,就有了我们的逆推,逆着推,后面的先考虑是否选 i i i ,那么前面的 d p [ j ] dp[j] dp[j] 就没有被污染了。

#include<bits/stdc++.h>

using namespace std;
const int maxn = 1e3 + 10;
int dp[maxn], w[maxn], v[maxn], sta[maxn];

signed main() {
    int n, m;
    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 = m; j >= 1; j--) {
            if (j >= v[i]) {
                dp[j] = max(dp[j], dp[j - v[i]] + w[i]);
            }
        }
    }
    cout << dp[m] << endl;
    return 0;
}

闲着无聊 写一篇二进制优化dp玩玩:
题目链接

有 N 种物品和一个容量是 V
的背包。
第 i种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。

第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i种物品的体积、价值和数量。
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10

二进制优化背包的前提是你要学会01背包和多重背包。

01背包很简单吧,第i件物品,我选或是不选,当前最多能拿到多少钱(价值)。
然后我们再进行第i件物品的选择。

对于多重背包来说 ,我门对前i种物品进行选择,对于第i种物品来说,我会尝试不选,看看现在我的钱(价值)有没有增多。
选择一个,看看现在我的钱(价值)有没有增多。
再看看选两个有没有增多,再看看选三个有没有增多,直到我的包太小装不下了,就完了。

然后我们再进行i+1种物品的选择。

有没有发现有点相似?

如果我们把多重背包拆开了,一种物品有n个,我们就拆成n个独立的物品。
卧槽,这不就是01背包吗。

然后我又开始搜寻宝藏了。
然后我发现,由于我把多重背包拆开了,有时候就会遇到,有1023个相同的物品。但我只能一次一次的进行选择,yes or no,进行1023次。
Oh my god !! 明明它们都是相同的,我就不能简化吗,为什么要这样折磨我。

我不知道从哪里得到了一个结论:
任何数都可以用多个不同的2的指数进行表示。
比如 7=1+2+4
11=8+1+2

然后我就突然得到了上帝的智慧,于是我把这1023个相同物品分成了1个,2个,4个,8个,16个,32个,64个,128个,256个,512个。总共10堆,刚好分完。哇,少了好多。

如果我现在背包可以装54个这样的物品,很显然我装满就是价值最大的时候了。
咦,这上面并没有54呢,但是我一个一个装是可以装到54的呢。
别急,
还记得我们的结论吗,54=32+16+4+2;
我选了54个物品就相当于是我选了32,16,4,2,这是可以组合的,那么对于dp来说它并不会管你如何组合,只会问你能不能组合。

我们对这一数进行dp时,其实我们已经讨论了1-1023所有的数。

哈哈哈,写的可能不是很好,但已经尽力了,这是我所能理解的二进制优化,当然还有点漏洞,比如我故意取了一个特殊值,1023,它恰好被分完了,如果是1024能? 我就不细说了,欢迎大家讨论。

后面是题解:

首先就是拆分多重背包,变成01背包,相同的物品进行二进制优化拆分,会拆成很少几个部分。
所有拆分当然是先要预处理啊:

    int len=1;
    for (int i = 1; i <= n; i++) {
        int z = 1;
        for (int j = 0; s[i] >(z<<j); s[i] -= (z<<j),j++ ) {

           q[len]=(z<<j)*v[i];
           p[len++]=(z<<j)*w[i];
        }
        q[len]=s[i]*v[i];
        p[len++]=s[i]*w[i];
    }

预处理是我随意写的,可能写的不是很好看。

完整代码:

#include<bits/stdc++.h>

using namespace std;
#define  int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 2e4 + 10;

int lowbit(int x) { return x & -x; }

bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

int w[maxn];
int v[maxn];
int s[maxn];
int dp[10005];
int q[maxn],p[maxn];
signed main() {
    int t;
    int n, c;
    cin >> n >> c;
    rep(i, 1, n) {
        cin >> v[i] >> w[i] >> s[i];
    }
    int len=1;
    for (int i = 1; i <= n; i++) {
        int z = 1;
        for (int j = 0; s[i] >(z<<j); s[i] -= (z<<j),j++ ) {

           q[len]=(z<<j)*v[i];
           p[len++]=(z<<j)*w[i];
        }
        q[len]=s[i]*v[i];
        p[len++]=s[i]*w[i];
    }
    for (int i = 1; i < len; i++) {
        for (int j = c; j >= q[i]; j--) {
            dp[j]=max(dp[j],dp[j-q[i]]+p[i]);
        }
    }
    cout << dp[c] << endl;
    return 0;
}

/***********************************************************************/

换个写法,效果一样:

#include<bits/stdc++.h>

using namespace std;
#define  int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 1e3 + 10;

int lowbit(int x) { return x & -x; }

bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

int w[maxn];
int v[maxn];
vector<int> a[100000];
int s[maxn];
int dp[100005];

signed main() {
    int t;
    int n, c;
    cin >> n >> c;
    rep(i, 1, n) {
        cin >> v[i] >> w[i] >> s[i];
    }
    for (int i = 1; i <= n; i++) {
        int z = 1;
        for (int j = 0; s[i] >= (z<<j); s[i] -=(z<<j) ,j++) {

           a[i].push_back((z<<j));

        }
        a[i].push_back(s[i]);
    }
    for (int i = 1; i <= n; i++) {
            int len = a[i].size();
            for (int k = 0; k < len; k++) {
                for (int j = c; j >= v[i]; j--)
                if (j - a[i][k] * v[i] >= 0)
                    dp[j] = max(dp[j], dp[j - a[i][k] * v[i]] + a[i][k] * w[i]);
            }
    }
    cout << dp[c] << endl;
    return 0;
}

如果遍历顺序不同,会产生另一种dp:
一种物品有n种取法,n种取法种只能取一次,产生的最大价值。

#include<bits/stdc++.h>

using namespace std;
#define  int long long
#define rep(i, x, y) for(auto i=(x);i<=(y);++i)
#define dep(i, x, y) for(auto i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const long long mod = 1e9 + 7;
const int maxn = 1e3 + 10;

int lowbit(int x) { return x & -x; }

bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}

int w[maxn];
int v[maxn];
vector<int> a[100000];
int s[maxn];
int dp[100005];

signed main() {
    int t;
    int n, c;
    cin >> n >> c;
    rep(i, 1, n) {
        cin >> v[i] >> w[i] >> s[i];
    }
    for (int i = 1; i <= n; i++) {
        int z = 1;
        for (int j = 0; s[i] >= (z<<j); s[i] -=(z<<j) ,j++) {

           a[i].push_back((z<<j));

        }
        a[i].push_back(s[i]);
    }
    for (int i = 1; i <= n; i++) {
            int len = a[i].size();
        for (int j = c; j >= v[i]; j--)
            for (int k = 0; k < len; k++) {

                if (j - a[i][k] * v[i] >= 0)
                    dp[j] = max(dp[j], dp[j - a[i][k] * v[i]] + a[i][k] * w[i]);
            }
    }
    cout << dp[c] << endl;
    return 0;
}


                ————————我是邪恶的分界线——————————————

题目链接

这一篇解释的可能更好——

背包九讲详解————真耐心的大佬
题意:一个数轴有n个村庄,有m个邮局建在村庄上,问如何建可使得每个村庄到最近的邮局距离之和最小。

思路:
d p [ i ] [ j ] dp[i][j] dp[i][j]表示 i i i村庄 j j j邮局的距离和最小值。(日常套路设法)
首先注意,我们选前i个村庄并不考虑后面村庄的影响。
推下状态方程,发现并不好推。

首先它肯定要从 j − 1 j-1 j1推出。
前i个村庄选j-1个邮局选法有很多,需要全部遍历。
但我们还是发现并不好推这个东西,因为在添加新邮局时需要更新距离和最小,前面已经推出的 d p dp dp 状态转移到当前,值是不一样的,需要更新它。而这显然不合理的。
我们需要前面的状态转移到当前,前面的 d p dp dp 不需要经过加工,可以直接用。
那么我当前新加节点时, 在从前k个村庄j-1个邮局转移而来时,dp[k][j-1]不能进行变化,那么从k+1到i,我新加一个邮局,需要在这个段取得和值最小。怎么做?
中间加。证明很简单,自己去画一下就行了。
那么状态转移方程就是 d p [ i ] [ j ] = m i n ( d p [ i ] [ j ] , d p [ k ] [ j − 1 ] + X ) dp[i][j]=min(dp[i][j],dp[k][j-1]+X) dp[i][j]=min(dp[i][j],dp[k][j1]+X)
其中 X X X就很有意思了,就是之前说的,在k+1☞i这个区间加一个邮局,得到的最小值和,那么怎么加?前面说过,加中间就行。
所以我们可以预处理出来。 d [ i ] [ j ] d[i][j] d[i][j] 表示从i到j加一个邮局距离和最小值。

    rep(i,1,n-1){
        rep(j,i+1,n){
            int z=(j+i)/2;
            for(int k=i;k<=j;k++){
                d[i][j]+=abs(a[k]-a[z]);
            }
        }
    }

初始状态为无穷大,dp[0][0] 为0。

  memset(dp,0x3f,sizeof(dp));
   dp[0][0]=0;

完整代码:

#include <bits/stdc++.h>

using namespace std;
#define  int long long
#define rep(i, x, y) for(int i=(x);i<=(y);++i)
#define dep(i, x, y) for(int i=(x);i>=(y);--i)
#define gcd(a, b) __gcd(a,b)
const int mod = 1e9 + 7;
const int maxn = 3e2 + 10;

int lowbit(int x) { return x & -x; }

bool ispow(int n) { return (n & (n - 1)) == 0; }//O(1) 判断是否是 2^k(2的k次方)
int read() {
    int x = 0, f = 1;
    char c = getchar();
    while (c < '0' || c > '9') {
        if (c == '-') f = -1;
        c = getchar();
    }
    while (c >= '0' && c <= '9') x = x * 10 + c - '0', c = getchar();
    return x * f;
}
int dp[maxn][maxn];
int v[maxn],w[maxn];
vector<int>p[maxn];
int d[maxn][maxn];
int n, m;
int a[maxn];
signed main() {
    cin>>n>>m;
//    int root=0;
//    rep(i,1,n){
//        int s;cin>>s;
//        if(s==-1)root=i;
//        else
//        p[s].emplace_back(i);
//    }
    rep(i,1,n){
         cin>>a[i];
    }
    rep(i,1,n-1){
        rep(j,i+1,n){
            int z=(j+i)/2;
            for(int k=i;k<=j;k++){
                d[i][j]+=abs(a[k]-a[z]);
            }
        }
    }
    memset(dp,0x3f,sizeof(dp));
   dp[0][0]=0;

    for(int i=1;i<=n;i++){
        for(int j=1;j<=m&&j<=i;j++){
            for(int k=j-1;k<i;k++){
                dp[i][j]=min(dp[i][j],dp[k][j-1]+d[k+1][i]);
                 //cout<<dp[i][1]<<endl;
            }
        }
    }
    cout<<dp[n][m]<<endl;
    return 0;
}

分组背包问题:金明的预算
https://www.luogu.com.cn/problem/P1064
在这里插入图片描述
思路:简单分组背包,主件所带的附件不超过3个,那么组合方案数最多7个,60个物品,最多的组合数其实只有 60 × 7 4 \dfrac{60\times7}{4} 460×7。组件和附件作为同一组,分组背包时间复杂度大约为 O ( n ∗ m ) O(n*m) O(nm)

#include<bits/stdc++.h>

using namespace std;
const int maxn = 1e5 + 10;
int dp[100][maxn];
struct  node{
    int v,w;
};
vector<node>s[maxn];
signed main() {
   // freopen("P1064_1.in","r",stdin);
    int n, m;
    cin >> n >> m;
    unordered_map<int,int >mp;
    int cnt = 1;
    for(int i = 1;i <= m; i++){
        int x,y,z ;
        cin>>x>>y>>z;
        if(z == 0){
            mp[i] = cnt;
            s[cnt++].emplace_back(node{x,y});
        }
        else {
            s[mp[z]].emplace_back(node{x,y});
        }
    }
    for(int i = 1;i < cnt;i++){
        int len = s[i].size();
        vector<node>S;
        S.clear();
        S.emplace_back(node{s[i][0].v,s[i][0].v*s[i][0].w});
        for(int j = 1;j < len;j++){
            S.emplace_back(node{s[i][j].v+s[i][0].v, s[i][j].v*s[i][j].w+s[i][0].w* s[i][0].v});
            for(int k = j + 1;k <len ;k++){
                S.emplace_back(node{s[i][j].v + s[i][0].v + s[i][k].v, s[i][j].v*s[i][j].w + s[i][0].v*s[i][0].w+ s[i][k].w*
                                                                                                                  s[i][k].v});
                for(int l = k + 1;l <len ;l ++){
                    S.emplace_back(node{s[i][j].v + s[i][0].v + s[i][k].v + s[i][l].v,
                                        s[i][j].v*s[i][j].w + s[i][0].v*s[i][0].w + s[i][k].v*s[i][k].w+ s[i][l].v*s[i][l].w});
                }
            }
        }
        s[i] = S;
    }
    for(int i = 1;i< cnt ;i++){
        for(int j = 1;j<= n ;j++){
            int len = s[i].size();
            for(int k = 0;k < len;k++){
                if(j >= s[i][k].v) {
                    dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i - 1][j - s[i][k].v] + s[i][k].w));
                 //   cout << dp[i][j] << endl;
                }
                else dp[i][j] = max(dp[i - 1][j],dp[i][j]);
            }
        }
    }
    cout<<dp[cnt - 1][n]<<endl;
    return 0;
}

上面的代码实在太冗长了,接下来就开始玩代码了,反正今天状态不佳,估计学不了什么了。
来试试重载运算符 + + +
好样的,在我一番调试下,终于找到了正确的重载方式。
首先,物品价格可以直接重载,木有问题。
但是!,物品性价比如果直接相乘相加,那么两个物品相加木有问题,三个物品相加就会重复计数: s 1 + s 2 = v 1 ∗ w 1 + v 2 ∗ w 2 s_1+s_2=v_1*w_1+v_2*w_2 s1+s2=v1w1+v2w2
正确
s 1 + s 2 + s 3 = ( v 1 ∗ w 1 + v 2 ∗ w 2 ) × ( v 1 + v 2 ) + v 3 × w 3 s_1 +s_2+s_3=(v_1*w_1+v_2*w_2)\times(v_1+v_2)+v_3\times w_3 s1+s2+s3=(v1w1+v2w2)×(v1+v2)+v3×w3
那么怎么重载呢?
就本身的性价比不变,再加上新来的就行了:
s 1 + s 2 + s 3 = v 1 ∗ w 1 + v 2 ∗ w 2 + v 3 × w 3 s_1 +s_2+s_3=v_1*w_1+v_2*w_2+v_3\times w_3 s1+s2+s3=v1w1+v2w2+v3×w3
这样的话第一件物品就必须加的是性价比。
代码:

#include<bits/stdc++.h>

using namespace std;
const int maxn = 1e5 + 10;
int dp[100][maxn];
struct  node{
    int v,w;
    node operator +(const node &p)const{
        return node{p.v + v,p.w* p.v + w };
    }
};
vector<node>s[maxn];
signed main() {
    int n, m;
    cin >> n >> m;
    unordered_map<int,int >mp;
    int cnt = 1;
    for(int i = 1;i <= m; i++){
        int x,y,z ;
        cin>>x>>y>>z;
        if(z == 0){
            mp[i] = cnt;
            s[cnt++].emplace_back(node{x,y * x});
        }
        else {
            s[mp[z]].emplace_back(node{x,y});
        }
    }
    for(int i = 1;i < cnt;i++){
        int len = s[i].size();
        vector<node>S;
        S.clear();
        S.emplace_back(node{s[i][0].v,s[i][0].w});
        for(int j = 1;j < len;j++){
            S.emplace_back(s[i][0] + s[i][j]);
            for(int k = j + 1;k <len ;k++){
                S.emplace_back(s[i][0]+ s[i][j]  + s[i][k]);
                for(int l = k + 1;l <len ;l ++){
                    S.emplace_back(s[i][0] + s[i][j] + s[i][l] + s[i][k]);
                }
            }
        }
        s[i] = S;
    }
    for(int i = 1;i< cnt ;i++){
        for(int j = 1;j<= n ;j++){
            int len = s[i].size();
            for(int k = 0;k < len;k++){
                if(j >= s[i][k].v) {
                    dp[i][j] = max(dp[i][j], max(dp[i - 1][j], dp[i - 1][j - s[i][k].v] + s[i][k].w));
                }
                else dp[i][j] = max(dp[i - 1][j],dp[i][j]);
            }
        }
    }
    cout<<dp[cnt - 1][n]<<endl;
    return 0;
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值