DP(2)——背包问题

2025.10.7:

P5020 货币系统:

https://www.luogu.com.cn/problem/P5020

其实这个题很简单,思路就是先贪心一遍,让纸币从小到大,然后每个判断能否用前面的金额表示。

关于完全背包

和0/1背包差别在于:完全背包每一种物品都有无限个。

下面这个题的思路很自然:既然是有无数个,那我每一次都从“当前纸币金额a[i]”开始更新后面的状态,能用之前表示的就标记为1,那么到后面遇到1,自然这种金额就是冗余的,删掉就行,比如说3,6,10,19里面的6和19;

通过不断地|=,我们可以保证每一个物品取到无数个都纳入考虑,比如说dp10|=dp0,dp20|=dp10(这样就取了两次10)

dp19|=dp9(这样就取了三次3,dp9是之前在3的时候处理过的)

#include<bits/stdc++.h>
using namespace std;
int T,n;
int dp[25005],a[105];
int main() {
    cin>>T;
    while(T--) {
        memset(dp,0,sizeof(dp));
        dp[0]=1;    //注意初始化
        cin>>n;     int ans=n;
        for(int i=1;i<=n;i++)cin>>a[i];
        sort(a+1,a+1+n);
        for(int i=1;i<=n;i++) {
            if(dp[a[i]])ans--;      //删除多余的纸币
            else {
                for(int j=a[i];j<=a[n];j++) {   //当前金额,只会对从现在开始到最大金额的造成影响
                    dp[j]|=dp[j-a[i]];
                }
            }
        }
        cout<<ans<<endl;
    }
    return 0;
}

dp[j]表示j金额能否用前面的金额表示。

用第一个样例打个表,就很清晰的知道什么意思了。

这个状态转移,就相当于不断地用前面可以表示的金额,更新后面的数值。

2025.10.8:

https://www.luogu.com.cn/problem/P1757

关于分组背包:

和0/1背包的区别在于每一组的物品只能选一个。

那么我们很自然的想到:从每一组里面选出最优解,所以我们只需要加上第三层循环遍历这个组,然后每一次更新,取max之后,自然就默认了每一组只选一个的情况。

#include<bits/stdc++.h>
using namespace std;
int V,n,t,ans;      //t看有多少组
int dp[1005],a[105];    //a存每一组的物品数量
vector<int>w[105],v[105];   //用改良邻接矩阵存,节省空间
int main() {
    cin>>V>>n;
    for(int i=1;i<=n;i++) {
        int x,y,z;     cin>>x>>y>>z;
        t=max(t,z);
        a[z]++;
        w[z].push_back(x);      v[z].push_back(y);
    }
    for(int i=1;i<=t;i++) {
        for(int j=V;j>=0;j--) {
            for(int k=0;k<a[i];k++) {   //第三层循环包含着每一组只能选一个
                if(w[i][k]<=j) {
                    dp[j]=max(dp[j],dp[j-w[i][k]]+v[i][k]);
                }
            }
        }
    }
    cout<<dp[V]<<endl;
    return 0;
}

https://www.luogu.com.cn/problem/P1064

这个题目看上去很吓人,还是个绿色题目,其实本质仍然是简单运用0/1背包,只不过多了一个主件附件,但是因为附件数量最多不超过2,我们甚至不需要分组背包来做他,只需要枚举四种情况就行:1、只买主件   2、买主件和附件一    3、买主件和附件二    4、买主件和附件一、二(唉,太傻比了)

code:

值得注意的是这个son[][]数组的运用,son[i][0]同时作为指针和计数的功能,son[i][1]指向第一个附件的下标,son[i][2]指向第二个附件的下标。

当然不用这个数组,直接用结构体也是可以的。

然后就是状态转移这个部分,直接无脑四个if判断,满足条件就选,不断更新。

2025.10.9:

https://www.luogu.com.cn/problem/P2946https://www.luogu.com.cn/problem/P2946

本质:0/1背包变式

就是考察所有价值是f倍数的背包问题

思考:一开始想的是开dp[j]表示能凑到的价值,然后遍历所有的f的倍数累加,然后发现如果j开到n*Rj,那么时间一定是不够的。

再思考:要考察的是f的倍数,那么我们直接处理一下数组,不开到1e5这么大,我们只开到f-1,也就是dp[i][j]表示前i件能凑到%f剩余j的方案数,最后只需要输出dp[n][0]就行。

code:

#include<bits/stdc++.h>
using namespace std;
int n,f;
const int N=2020,F=1010,Mod=1e8;
int dp[N][F],a[N];
int main() {
    cin>>n>>f;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
        a[i]%=f;
    }
    for(int i=1;i<=n;i++) {
        dp[i][a[i]]++;  //每一行首先都先++
        for(int j=0;j<=f-1;j++) {
            dp[i][j]+=dp[i-1][j]+dp[i-1][(j+f-a[i])%f];
            dp[i][j]%=Mod;
        }
    }
    cout<<dp[n][0]<<endl;
    return 0;
}

这里注意:我在cin的时候把a[i]%=f了,那么在状态转移的时候,写了一个dp[i-1][(j+f-a[i])%f],就能凑出来0/1背包。

不过我之前犯蠢,cin的时候写的是a[i]%=Mod了,这实在是太傻比了。

2025.10.11:

P1156 垃圾陷阱:

https://www.luogu.com.cn/problem/P1156https://www.luogu.com.cn/problem/P1156

本质:并非0/1背包,但是有类似地方

关键:明白dp数组应该怎么设置(①方不方便枚举②有没有初始化的隐藏提示),状态转移应该正向转移,并且考虑吃/堆的影响

code:

# include<iostream>
# include<cstring>
# include<algorithm>
using namespace std;
struct p{
    int t,h,l;
}c[101];
int d,g;
int f[101];
bool cmp(p a,p b)
{
    return a.t<b.t;
}
int main()
{
    cin>>d>>g;
    for(int i=1;i<=g;i++)
        cin>>c[i].t>>c[i].l>>c[i].h;
    sort(c+1,c+1+g,cmp);
    f[0]=10;    //到高度j的最大生命值
    for(int i=1;i<=g;i++)
        for(int j=d;j>=0;j--)
            if(f[j]>=c[i].t)    //生命值足够,就对垃圾操作
            {
                if(j+c[i].h>=d)     //可以逃出
                {
                    cout<<c[i].t;   //直接输出i的时间,即立马堆立马逃出
                    return 0;
                }
                //吃或者不吃,都会有影响,所以写了这两行
                f[j+c[i].h]=max(f[j+c[i].h],f[j]);      //不吃垃圾,高度改变
                f[j]+=c[i].l;   //吃垃圾,高度不变
            }
    cout<<f[0];
    return 0;
}

打表也是很必须的:

P5322 排兵布阵:

https://www.luogu.com.cn/problem/P5322

本质:0/1背包         区别:有多个人

解决方案:显然设置dp[i][j]表示第i个城堡派出士兵数量<=j时的最大价值。

因此输入要输入成a[j][i](细节)

a[i][j]表示第i个城堡的第j个人派出士兵的数量,只要每行都sort一遍,这样考虑多个人的时候就可以用k枚举人的数量,当j>=a[i][k]时就直接+k*i的价值。(因为如果第k个满足,那么前面的肯定都满足),同时这个a[i][k]也能代表第i座城堡的空间(本质就是把二维处理成一维,)

所以状态转移:

if(j>=a[i][k])dp[j]=max(dp[j],dp[j-a[i][k]]+k*i);

 就类似这种:

×
××
××

     

code:

#include<bits/stdc++.h>
using namespace std;
#define int long long
int s,n,m;
int a[105][105],dp[20005];
signed main() {
    cin>>s>>n>>m;
    for(int i=1;i<=s;i++) {
        for(int j=1;j<=n;j++) {
            cin>>a[j][i];
            a[j][i]*=2;a[j][i]++;
        }
    }
    //对每一行sort
    for(int i=1;i<=n;i++)sort(a[i]+1,a[i]+1+s);
    for(int i=1;i<=n;i++) {
        for(int j=m;j>=0;j--) {
            for(int k=1;k<=s;k++) {
                if(j>=a[i][k])dp[j]=max(dp[j],dp[j-a[i][k]]+k*i);
            }
        }
    }
    cout<<dp[m]<<endl;
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值