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=1∑∣A∣pai,
S
B
=
∑
i
=
1
∣
B
∣
p
b
i
S_B=\sum\limits_{i=1}^{|B|}p^{b_i}
SB=i=1∑∣B∣pbi,求
m
i
n
∣
S
A
−
S
B
∣
min|S_A-S_B|
min∣SA−SB∣。其中
1
≤
n
,
p
≤
1
0
6
1\leq n,p\leq 10^6
1≤n,p≤106。由于最后的结果可能很大,只需返回%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;
}