CF1394A(枚举case贪心,倒着dp)

CF1394A(枚举case贪心,倒着dp)

题意

你在群里开 \(n\) 个玩笑,每个玩笑可以获得 \(a_i\) 快乐值,但是如果 \(a_i > m\) ,狗管理就会给你一发 \(d\) 天的口球。在禁言期间无法发言。

现在找到玩笑方案使得快乐值的和最大。

(每个玩笑仅可以说一次)

思路一(贪心)

如果给定 \(x\) 次说被禁言的玩笑,可以很简单构造出贪心解。因此只要枚举 \(x\) 对每种情况取 \(max\) 就行。


感觉和平时贪心套路有点差别,一般贪心都是找到一个贪心策略+边界case讨论或者二分+贪心check。这里则是因为直接贪会陷入分类讨论的漩涡。虽然不能二分答案,但我们可以 枚举case

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#include<map>
#include<stack>
#include<string>
#include<random>
#include<iomanip>
#define yes puts("yes");
#define inf 0x3f3f3f3f
#define ll long long
#define linf 0x3f3f3f3f3f3f3f3f
#define ull unsigned long long
#define endl '\n'
#define int long long
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
using namespace std;
mt19937 mrand(random_device{}());
int rnd(int x) { return mrand() % x;}
typedef pair<int,int> PII;
const int MAXN =10 + 2e5 ,mod=1e9 + 7;

void solve()
{    
    int n,m,d; cin >> n >> d >> m;
    vector<int> a,b;
    for(int i = 0;i < n;i += 1) {
        int t; cin >> t;
        if(t > m) a.push_back(t);
        else b.push_back(t);
    }
    if(a.size() == 0) {
        cout << accumulate(b.begin(),b.end(),0ll);
        return;
    }
    sort(a.begin(),a.end(),greater<int>());
    sort(b.begin(),b.end(),greater<int>());
    int sza = a.size(),szb = b.size();
    vector<ll> sa(sza + 1),sb(szb + 1);
    for(int i = 1;i <= sza;i += 1) sa[i] = sa[i - 1] + a[i - 1];
    for(int i = 1;i <= szb;i += 1) sb[i] = sb[i - 1] + b[i - 1];
    ll ans = 0;
    for(int i = (sza + d) / (d + 1);i <= sza ; i += 1) {
        if(n - (i - 1) * (d + 1) - max(1ll,sza - (i - 1) * (d + 1)) >= 0) {
            // cout << i << " " << n - (i - 1) * (d + 1) - max(1ll,sza - (i - 1) * (d + 1)) << endl;;
            ll res = sa[i] + sb[n - (i - 1) * (d + 1) - max(1ll,sza - (i - 1) * (d + 1))];
            ans = max(res,ans);
        }
    }
    cout << ans;
}
signed main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);

    //int T;cin>>T;
    //while(T--)
        solve();

    return 0;
}

思路二(dp)

做dp的基础一定要知道初始状态解,这里的初始状态在最后一次玩笑,最后一次一定选择快乐值最大的玩笑,因为之后被禁言也无所谓,所以这里是无代价的。这样就有了倒着dp的基础。进而的我们可以把序列翻转,变成一个前缀的问题,就是一个很顺眼的dp了。

对这个问题其实就是从两个集合选数的问题,我们考虑朴素定义 \(dp[i][j][k]\) 表示考虑前 \(i\) 个玩笑用了 \(j\) 个禁言玩笑和 \(k\) 个非禁言玩笑的答案。这显然是可做的,但会时空都不可以接受

这里的优化技巧是考虑进行多个dp

我们修改定义为:\(dp[i][0/1/2]\) 分别表示答案,用了前 \(dp[i][1]\) 的禁言玩笑和 \(dp[i][2]\) 的非禁言玩笑。

考虑转移

对位置 \(i\) 分三类

  • 开一个禁言玩笑,则它会从 \(i - d - 1\) 转移过来并且保证此时有玩笑可开
  • 开一个非禁言玩笑,它会从 \(i - 1\) 转移并且保证此时有玩笑可开
  • 不开玩笑,或者认为做完dp后这里随便填一个,反正贡献为0

按上面三类做如下dp即可

f[i][0] = f[i - 1][0];
        f[i][1] = f[i - 1][1];
        f[i][2] = f[i - 1][2];
        if(f[i - 1][2] < szb ) {
            f[i][0] = f[i - 1][0] + b[f[i - 1][2]];
            f[i][1] = f[i - 1][1];
            f[i][2] = f[i - 1][2] + 1;
        }
        if(f[i - 1][1] < sza and i - d - 1 > 0) {
            if(f[i][0] < f[i - d - 1][0] + a[f[i - d - 1][1]]) {
                f[i][0] = f[i - d - 1][0] + a[f[i - d - 1][1]];
                f[i][1] = f[i - d - 1][1] + 1;
                f[i][2] = f[i - d - 1][2];
            }
        }

完整代码:

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<set>
#include<queue>
#include<map>
#include<stack>
#include<string>
#include<random>
#include<iomanip>
#define yes puts("yes");
#define inf 0x3f3f3f3f
#define ll long long
#define linf 0x3f3f3f3f3f3f3f3f
#define ull unsigned long long
#define endl '\n'
#define int long long
#define rep(i,a,n) for(int i = a;i <= n;i++)
#define per(i,n,a) for(int i = n;i >= a;i--)
using namespace std;
mt19937 mrand(random_device{}());
int rnd(int x) { return mrand() % x;}
typedef pair<int,int> PII;
const int MAXN =10 + 2e5 ,mod=1e9 + 7;
ll f[MAXN][3];
void solve()
{    
    int n,m,d; cin >> n >> d >> m;
    vector<int> a,b;
    for(int i = 0;i < n;i += 1) {
        int t; cin >> t;
        if(t > m) a.push_back(t);
        else b.push_back(t);
    }
    if(a.size() == 0) {
        cout << accumulate(b.begin(),b.end(),0ll);
        return;
    }
    sort(a.begin(),a.end(),greater<int>());
    sort(b.begin(),b.end(),greater<int>());
    int sza = a.size(),szb = b.size();
    f[1][0] = a[0];
    f[1][1] = 1;
    f[1][2] = 0;
    
    for(int i = 2;i <= n;i += 1) {
        f[i][0] = f[i - 1][0];
        f[i][1] = f[i - 1][1];
        f[i][2] = f[i - 1][2];
        if(f[i - 1][2] < szb ) {
            f[i][0] = f[i - 1][0] + b[f[i - 1][2]];
            f[i][1] = f[i - 1][1];
            f[i][2] = f[i - 1][2] + 1;
        }
        if(f[i - 1][1] < sza and i - d - 1 > 0) {
            if(f[i][0] < f[i - d - 1][0] + a[f[i - d - 1][1]]) {
                f[i][0] = f[i - d - 1][0] + a[f[i - d - 1][1]];
                f[i][1] = f[i - d - 1][1] + 1;
                f[i][2] = f[i - d - 1][2];
            }
        }
    }    
    cout << f[n][0];
}
signed main()
{
    ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);

    //int T;cin>>T;
    //while(T--)
        solve();

    return 0;
}

感觉把这个题意改成,有两个集合,第一个集合选数和上一个距离不小于 \(d\) 第二个集合则没有限制,一共 \(n\) 个空可以选。这样的题意可能就能一眼dp了。

这题把初始状态翻了一下而且原题说的是安排一个最优排列,反而有点迷惑性没反应过来是dp

posted @ 2022-07-14 19:35  Mxrurush  阅读(15)  评论(0编辑  收藏  举报