Codeforces Round 647 B题 Johnny and Grandmaster

Johnny and Grandmaster

       本题是Codeforces Round 647 B题,也是Div2的E题。当时Div2大概只有两百人过,但其实后来发现也不是很难,并没有什么特别高深的思想方法。思路来源借鉴了heyuhhh的题解,贴个链接heyuhhh的blog
       原题就不贴了。题目的大意是给 n n n个数和底数 p p p,要求把这 n n n个数分成两个集合 A A A B B B,设 S A = ∑ i = 1 ∣ A ∣ p a i S_A=\sum\limits_{i=1}^{|A|}p^{a_i} SA=i=1Apai S B = ∑ i = 1 ∣ B ∣ p b i S_B=\sum\limits_{i=1}^{|B|}p^{b_i} SB=i=1Bpbi,求 m i n ∣ S A − S B ∣ min|S_A-S_B| minSASB。其中 1 ≤ n , p ≤ 1 0 6 1\leq n,p\leq 10^6 1n,p106。由于最后的结果可能很大,只需返回%1000000007的结果。另外,注意是要总的差最小的情况,而非取模之后的值最小。比如说两种情况分别得到的差是2和1000000008,也应该取2的情况。
       比赛的时候满脑子都是dp,因为想到要让两个集合的差最小,自然就是要最接近所有数和的一半,是一个典型的背包问题。但这里指数 n n n的值太大了,算出具体数值似乎比较棘手,顿时就没思路了。
       其实仔细用数学直觉想一想这个问题。我们知道指数是上升很快的,如果将最大的元素放在 B B B里,为了差最小,自然就需要很多个小的元素放在 A A A里去抵消,然而这个抵消就需要分几种情况讨论。由于排序不影响结果,我们将数组 a a a排序后从大到小处理,假设首先处理 a i a_i ai,我们将 a i a_i ai放入了 B B B,然后把接下来的元素放进 A A A里去抵消,可能出现如下情况:
       第一种,放进 A A A一个元素之后, S A < S B S_A<S_B SA<SB。这个时候直接继续从 A A A里取元素就可以了,争取能够凑的更大。
       第二种,放进 A A A一个元素之后, S A = S B S_A=S_B SA=SB。这是很舒适的情况,把大的给抵消掉了。这个时候就应该回到最开始,再把剩余最大的放入 B B B,相似处理。为什么可以这么贪心处理呢?直觉上是没问题的,这里给出一个还凑合的证明。可以假设存在一个更优解,里面 a i a_i ai是被其他一些元素抵消掉的。具体的,我们把 a a a分成四部分。集合 S 1 S_1 S1是两种抵消 a i a_i ai的策略都用到的元素, S 2 S_2 S2是仅贪心用到的, S 3 S_3 S3是仅假设的更优解用到的元素, S 4 S_4 S4是两种抵消策略都没用到的元素。贪心处理会剩余 S 3 , S 4 S_3,S_4 S3,S4,更优解剩下 S 2 , S 4 S_2,S_4 S2,S4,其中 S 2 S_2 S2求和等于 S 3 S_3 S3求和,而且对 S 2 S_2 S2每个元素,可以对 S 3 S_3 S3的一个或多个元素,构成单射,使得两两的和相等(注意到这里只含有 p p p的次方形式, S 2 S_2 S2里面一个更大的元素一定可以替换成若干个 p p p的更小次方之和)。也即,更优解剩下来的每一种划分都一定可以用 S 3 , S 4 S_3,S_4 S3,S4表示出来,所以贪心可以得到最优解。
       还有第三种情况,就是 S A < S B S_A<S_B SA<SB。但其实这是不可能的,因为加到等于 S B S_B SB的情况时,就已经进入情况二处理了。这也是比较容易忽略的一个地方。
       所以算法就出来了。然后有几个比较trick的地方,一个是关于幂之和 S S S和这种抵消关系的表示上,直接表示和比较困难,我们可以对大元素进行拆分,即 a i a_i ai放到集合 B B B里之后,对当前遍历到的元素 a n o w a_{now} anow,把 a i a_i ai拆分成若干个 a n o w a_{now} anow,然后消去一个。然后如果发现拆的个数已经大于 n n n了,就不用再拆了,因为我们知道剩下所有的加在一起也打不过 a i a_i ai了,直接continue就可以了。情况二等于的条件,就等价于拆完、抵消之后, a i a_i ai等价的 a n o w a_now anow个数等于0,就可以进入下一次循环,放入新的 a i a_i ai。还有在最后不可避免的要算幂,需要用一下快速幂,最后写下来代码并不复杂,代码如下:

#include <iostream>
#include <stdio.h>
#include <string>
#include <cmath>
#include <algorithm>
#include <cstring>
#include <string.h>
#include <vector>
using namespace std;
int n,p,t;
int mod=1e9+7;
typedef long long ll;
ll quickpow(ll a,ll b){
    ll res=1;
    while(b){
        if(b&1) res=res*a%mod;
        a=a*a%mod;
        b>>=1;
    }
    return res;
}
int main() {
    cin>>t;
    while(t--){
        cin>>n>>p;
        vector<int> a(n);
        for(int i=0;i<n;i++) cin>>a[i];
        if(p==1){
            if(n&1) cout<<1<<endl;
            else cout<<0<<endl;
            continue;
        }
        sort(a.begin(),a.end());
        vector<int> l,r;
        while(a.size()>0){
            r.push_back(a.back());
            a.pop_back();
            ll nownum=1;
            int last=r.back();
            for(int i=a.size()-1;i>=0;i--){
                l.push_back(a[i]);
                int d=last-a[i];
                last=a[i];
                a.pop_back();
                while(d&&nownum<n){
                    nownum*=p;
                    d--;
                }
                nownum--;
                if(d==0&&nownum==0) break;
            }
        }
        int res=0;
        for(auto x:l){
            res=(res+mod-quickpow(p,x))%mod;
        }
        for(auto x:r){
            res=(res+quickpow(p,x))%mod;
        }
        cout<<res<<endl;
    }
    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值