背包类Dp问题

一.F - Programming Contest (atcoder.jp)

        (1)题目大意

        给定你一个长度为n的序列,再给定你一个t,问你在这个n个数中得到的不超过t的和的最大值是多少?

         (2)解题思路

                很明显,直接状压枚举会超时,而直接dp会暴内存,因此我们考虑综合一下,对于前面一半我们采用状压存储所枚举的所有值,放入vector中,然后排序,对于后半部分的值,我们考虑直接状压枚举,然后跟vector中的值去比较能否二分到一个t-tmp的值。

        (3)代码实现

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 50;
int a[N];
void solve()
{
    int n,t;
    cin >> n >> t;
    ll ans = 0,s = 0;
    vector <int> v; 
    for(int i = 0;i < n;i++) cin >> a[i];
    int h = n / 2,p = n - n / 2;
    for(int i = 0;i < (1 << h);i ++) {
        s = 0;
        for(int j = 0;j < h;j++) 
            if(i >> j & 1) 
                s += a[j];
        if(s <= t) v.push_back(s);
    }
    sort(v.begin(),v.end());
    v.erase(unique(v.begin(),v.end()),v.end());
    for(int i = 0;i < (1 << p);i++) {
        s = 0;
        for(int j = 0;j < p;j++) 
            if(i >> j & 1) 
                s += a[j + h];
        if(s > t) continue;
        auto it = lower_bound(v.begin(),v.end(),t - s);
        if(it == v.end()) it --;
        if(*(it) + s <= t) ans = max(ans,*(it) + s);
        else if(it != v.begin()) it --,ans = max(ans,*(it) + s);
    }
    cout << ans << endl;
}
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    int T;
    T = 1;
    while(T --) solve();
    return 0;
}

二.Problem - I - Codeforces

        (1)题目大意

                给定你一串牌,每张卡都有一个数字标签ti,即点数。此外,每张卡都有一个值vi。鲍勃必须从这n张牌中选择一些牌,并将所选的牌分成两组,使两组中的牌的点数之和相等。然而,根据鲍勃的角色技能,在选择这两组牌之前,他最多可以选择k个不同的牌,并将它们的点数加倍。换句话说,他可以选择一个序列{a1,a2,⋯⋯,ar},(1≤a1<a2<⋯⋯<ar≤n,0≤r≤k),对于每个i(1≤i≤r),将tai变成2tai。之后他可以继续选择这两组。他们想知道他们最终得到的牌的价值的最大可能的总和。换句话说,在所有有效方案中确定最大的。

         (2)解题思路

                题目很长,但是可以想一想知道这是一个二维费用的背包问题,但要处理出点数平衡这个状态有点新奇。对于平衡这个状态,我们可以定义减去和加上这两个状态进行抵消,然后看题目发现ti最多13,n最大100,也就是说最多会有-2600,到2600这么多目标值,因此我们考虑把初始状态定义为2600。

                然后分析状态转移方程

                1.若不进行那此张牌

                        dp[i][j][k] = dp[i - 1][j][k];

                2.若拿此张牌,给到Alice,但是不进行加倍操作

                       dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j + w[i]][k] + v[i]);

                3.若拿此张牌,给到Alice,但是进行加倍操作

                        dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j + 2 * w[i]][k - 1] + v[i]);        

                4.若拿此张牌,给到Bob,但是不进行加倍操作

                        dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j - w[i]][k] + v[i]);

                5.若拿此张牌,给到Bob,但是进行加倍操作

                        dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j - 2 * w[i]][k - 1] + v[i]);

                答案就是max(dp[n][2600][i](i:1-k))

        (3)代码实现

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 105;
ll dp[N][5205][N];
int w[N],v[N];
int main()
{
    memset(dp,-0x3f,sizeof(dp));
    dp[0][2600][0] = 0;
    ll ans = 0;
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i = 1;i <= n;i++) scanf("%d%d",&v[i],&w[i]);
    for(int i = 1;i <= n;i++) {
        for(int j = 0;j <= 5200;j++) {
            for(int k = 0;k <= m;k++) {
                dp[i][j][k] = dp[i - 1][j][k];
                if(j + w[i] <= 5200) dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j + w[i]][k] + v[i]);
                if(k && j + 2 * w[i] <= 5200) dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j + 2 * w[i]][k - 1] + v[i]);
                if(j - w[i] >= 0) dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j - w[i]][k] + v[i]);
                if(k && j - 2 * w[i] >= 0) dp[i][j][k] = max(dp[i][j][k],dp[i - 1][j - 2 * w[i]][k - 1] + v[i]);
            }
        }
    }
    for(int i = 0;i <= m;i++) ans = max(ans,dp[n][2600][i]);
    printf("%lld",ans);
    return 0;
}

三.F - Potion (atcoder.jp)

        (1)题目大意

                给你k个材料,和一个能量x,每个裁量都有一个能量,高桥先生能任意拿k个材料,并且把他们合起来,最后必须要保证能到x,(注意k个材料合成后,每秒会有k的能量生长),请问最少的时间是多少?

         (2)解题思路

                对于x的范围有10^18,因此我们并不能从x开始下手,我们观察到N只有100,也就是说,我最多可以拿n个,实际上我们可以通过个数下手,定义dp[i][j]为有i个,%k值为j的最大值。

若dp[k][x % k]==-1说明这个状态没有,则不可能成为答案,否则(x - dp[k][x % k]) / k就是答案。

        (3)代码实现

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 110;
int a[N];
int main()
{
    int n;
    long long x;
    cin >> n >> x;
    for(int i = 1;i <= n;i++) cin >> a[i];
    ll ans = 1e18;
    for(int k = 1;k <= n;k++) {
        vector <vector<ll>> dp(k + 1,vector<ll>(n,-1));
        dp[0][0] = 0;
        for(int i = 1;i <= n;i++) {
            for(int j = k - 1;j >= 0;j--) {
                for(int t = 0;t < k;t++) {
                    if(dp[j][t] == -1) continue;
                    ll nxt = dp[j][t] + a[i];
                    dp[j + 1][nxt % k] = max(dp[j + 1][nxt % k],nxt);
                }
            }
        }
        ll st = dp[k][x % k];
        if(st == -1) continue;
        ans = min(ans,(x - st) / k);
    }
    cout << ans << endl;
    return 0;
}

四.意外惊喜 - 力扣 (LeetCode) 竞赛

        (1)题目大意

         (2)解题思路

                首先我们需要发现一个贪心的性质,就是我只要取这个包,那我一定要尽可能拿完,也就是说我们可以把每个礼包看作一个整体,做一个01背包,但是这里又有一个limit,而且礼包大小不相同,因此我们需要考虑拿完一些整的背包后,剩下的该拿哪一个。如果直接枚举会是n*n*limit,我们想我们肯定要枚举最后一个这是跑不掉的,但是我们枚举的过程dp是不是可以优化一下呢?因为我们如果计算左边的背包,那么我们枚举右边区间的时候,实际上上只需要加上左边背包的价值,因此这题的思路就是贪心+分治优化01背包

        (3)代码实现

#include "bits/stdc++.h"
using namespace std;
using ll = long long;
const int N = 2e3 + 10;
ll dp[25][N],c[N],s[N][N],ans;
int n,k;
void do_dp(int f,int l,int r)
{
    for(int i = l;i <= r;i++) 
        for(int j = k;j >= c[i];j--) 
            dp[f][j] = max(dp[f][j],dp[f][j - c[i]] + s[i][c[i]]);
}
void init(int f)
{
    for(int i = 0;i <= k;i++) dp[f][i] = dp[f - 1][i];
}
void dfs(int f,int l,int r)
{
    if(l == r) {
        ans = max(ans,dp[f][k]);
        for(int i = 1;i <= c[l] && i <= k;i++) ans = max(ans,dp[f][k - i] + s[l][i]);
        return ;
    }
    int mid = (l + r) >> 1;
    init(f + 1);
    do_dp(f + 1,l,mid);
    dfs(f + 1,mid + 1,r);
    
    init(f + 1);
    do_dp(f + 1,mid + 1,r);
    dfs(f + 1,l,mid);
}
int solve()
{
    for(int i = 1;i <= n;i++) {
        for(int j = 1;j <= c[i];j++)
            s[i][j] += s[i][j - 1];
    }
    dfs(1,1,n);
    return ans;
}
class Solution {
public:
    ll brilliantSurprise(vector<vector<int>>& present, int limit) {
        k = limit;
        n = ans = 0;
        memset(dp,0,sizeof(dp));
        for(auto x:present) {
            c[++ n] = x.size();
            for(int i = 0;i < c[n];i++) s[n][i + 1] = x[i];
        }
        return solve();
    }
};

五.D-清楚姐姐学01背包(Hard Version)_2023牛客寒假算法基础集训营4 (nowcoder.com)

        (1)题目大意

                问你去掉某个背包的最大值为vmax,若加上当前这个背包的最大值为vmax2,若vmax<vmax2,则当前背包为必需背包,问你有n个背包,每个背包达到必需背包的最小价值要增加多少?

         (2)解题思路

                对于去掉某个背包这个限制条件我们可以,直接从前往后dp1一下计算前i个dp1的最大值,从后往前dp2一下最大值,然后对于去掉每个背包我们直接枚举重量为dp1[i-1][j]+dp2[i+1][j]的价值,然后用加上这个背包的最大价值减去去掉这个背包的最大价值+1,这个就是最小价值了。

        (3)代码实现

#include "bits/stdc++.h"
#define rep(i,z,n) for(int i = z;i <= n; i++)
#define per(i,n,z) for(int i = n;i >= z; i--)
#define PII pair<int,int>
#define fi first
#define se second
#define vi vector<int>
#define vl vector<ll>
#define pb push_back
#define sz(x) (int)x.size()
#define all(x) (x).begin(),(x).end()
using namespace std;
using ll = long long;
const int N = 5e3 + 10;
ll dp1[N][N],dp2[N][N];
int w[N],v[N],m,n;
ll calc(int idx)
{
	ll lim=0,ans=2e18;
	rep(j,0,m) lim=max(lim,dp1[idx-1][j]+dp2[idx+1][m-j]);
	rep(j,0,m){
		ll tv;	
		if(j>=w[idx]){
			tv=dp1[idx-1][j-w[idx]]+v[idx]+dp2[idx+1][m-j];
			ans=min(ans,max(lim-tv+1,0ll));
		}
		if(m-j>=w[idx]){
			tv=dp1[idx-1][j]+dp2[idx+1][m-j-w[idx]]+v[idx];
			ans=min(ans,max(lim-tv+1,0ll));
		}
	}
	return ans;
}
void solve()
{
	cin>>n>>m;
	rep(i,1,n)cin>>w[i]>>v[i];
	rep(i,1,n) rep(j,0,m){
		dp1[i][j]=max(dp1[i-1][j],dp1[i][j]);
		if(j>=w[i]) dp1[i][j]=max(dp1[i-1][j-w[i]]+v[i],dp1[i][j]);
	}
	per(i,n,1) rep(j,0,m){
		dp2[i][j]=max(dp2[i+1][j],dp2[i][j]);
		if(j>=w[i]) dp2[i][j]=max(dp2[i+1][j-w[i]]+v[i],dp2[i][j]);
	}
	rep(i,1,n) cout<<calc(i)<<endl;
}	
int main()
{
    ios::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    int T = 1;
    // cin >> T;
    while(T --) solve();
    return 0;
}

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值