AGC057D Sum Avoidance 做题笔记
距离独立做出这道黑题就差一点点,就一点点。
题意简述
给定两个正整数 \(n, k\),求考虑一个集合 \(S\subseteq \{1, 2, \dots n-1\}\),满足:
- 不存在数列 \(a_1, a_2, \dots, a_t\),\(a_i\in S\),\(\sum_{i=1}^t a_i=n\)
- 满足第一个条件的基础上,最大化 \(|S|\)。
- 满足最大化 \(|S|\) 的基础上,将 \(S\) 中的数排序得到的数列的字典序最小。
要你输出 \(S\) 中第 \(k\) 大的数,或者指出 \(|S|>k\)。
\(k<n\leq 10^{18}\),有 \(1000\) 组数据。
解法
可以写一个程序枚举一下所有的 \(S\),尝试找一些规律,你会发现如下的性质:
- \(|S|=\left \lfloor \frac{n-1}{2} \right \rfloor\)。
- 若\(a, b\in S, a+b<n \text{则}a+b\in S\)
- 设 \(d\) 是最小的,不是 \(n\) 约数的正整数,则 \(kd\in S(k=1, 2, \dots, \left \lfloor \frac{n}{d} \right \rfloor)\)。
你可以直接把这三个猜想当作结论用,但是证明也是十分容易的。
对于第一个性质的证明:考虑 \(\left \lfloor \frac{n-1}{2} \right \rfloor\) 个二元组 \((i, n-i)\)($1\leq i\leq\left \lfloor \frac{n-1}{2} \right \rfloor $)。每个二元组中最多选择一个数,因此 \(|S|\leq\left \lfloor \frac{n-1}{2} \right \rfloor\),又因为 \(\{\left \lfloor \frac{n}{2} \right \rfloor+1, \left \lfloor \frac{n}{2} \right \rfloor+2, \dots n-1\}\) 符合第一个条件,说明等号可以取到。
对第二个性质十分显然,将 \(a+b\) 放入,不会对第一个约束条件产生影响,还能增加 \(S\)。
对于第三个性质的证明,只需证明 \(d\in S\) 即可。(然后可以用第二个性质证明 \(d\) 的倍数都在 \(S\) 中)注意到 \(1, 2, \dots d-1\) 都整除 \(n\),则它们都不能选。而如果选择了 \(d\),可以考虑选出所有 \(d\) 的倍数,以及所有大于 \(\left \lfloor \frac{n}{2} \right \rfloor\) 的,和 \(n\) 的差不是 \(d\) 的倍数的数。容易证明这样的取法能够让 \(|S|\) 达到上界。
这是启发我们对着模 \(d\) 的剩余系做,具体来说,只需要确定模 \(d\) 的每一个剩余系的数在 \(S\) 中出现的最小值。
既然要最小化字典序,则可以考虑贪心。
可以维护 \(f_i\) 表示已经选出的数中能凑出的数中,模 \(d\) 余 \(i\) 的最小数。
每次加入一个数 \(v\),都令 \(\forall i, f_i\leftarrow \min_{j=0}^{d-1}f_{(i-j(v\mod d))\mod d}+jv\)。
但有一个问题,就是如果直接贪心,不一定能保证 \(|S|\) 最大,为此我们需要如下的结论。
结论:如果一个集合 \(A\) 满足约束一和性质二,那么一定能存在 \(A\subseteq S\) 对于某个达到大小上界的 \(S\)。
这个结论表明:直接贪心是对的!
证明也很简单,考虑 \(|S|\) 个二元组,如果一个二元组两个数都不在 \(A\) 中,那么就选较大的那个,这样一定能取到上界,并且显然不会违背约束一。
所以我们直接贪就可以了,具体来说,方法如下:
- 考虑所有还没有选择的剩余系,对于每一种剩余系,找到其中能加入当前集合 \(A\) 的最小数(如何判断能否加入 \(A\) 中?只需保证不等式 \(f_{n\mod d}>n\) 成立)
- 从所有候选方案中找到最小的那个候选方案,把它加入 \(A\)。
- 更新 \(f\)。
然后这道题就做完了。
复杂度 \(O(d^3)\)。
总结
几个性质都是平凡的,难点在于如何设法保证 \(|S|\) 上界的取到,我感觉很难保证约束一的情况下,达到上界,所以没有考虑贪心。但是其实字典序问题很有可能用贪心,如果我反向思考,去尝试贪心,就应该能够发现贪心是对的。
第二个方法是在注意到 \((x, n-x)\) 这两个数中必须选一个后,就可以考虑取出所有前一半的数,后一半的数如何选择完全依赖前一半的数,这样只需要最小化前一半数的字典序,并保证前一半数合法即可。那么此时再考虑到按照 \(d\) 剩余系分类做,那么想到贪心就是自然而然的。
那么如果我没有想到剩余系的性质,而先想到取前一半的性质,然后再考虑贪心的时候再想到利用剩余系,反而能做出来。但是发现剩余系后,我就一直在对着剩余系上的 DP 做,这样就没有考虑到关于集合上界的结论。
其实就算得到一个很好的结论,也不能只对着这个结论一直做,思路不一定呈现线性,一直推导就能到达终点,有可能需要暂时放下这个结论,去思考一些别的方面的内容,再结合起来。
代码
#include <iostream>
#include <random>
using namespace std;
typedef __int128_t ll;
#define rep(i, a, b) for(ll i=a;i<=b;i++)
#define rrep(i, a, b) for(ll i=a;i>=b;i--)
const ll D=109;
ll S;
ll d;
ll g[D];
ll f[D];//f[i]表示能凑出的模d余i的最小数
//加入v=(kd+x)后,f的变化
//f[i]<-min((原先的)f[(i-jx)\mod d]+jv)(j=0, 1, ..., d-1)
bool cho[D];//表示模d余i的剩余类有没有选过
//关键在于弄清楚每一个剩余类在哪里被一次选中
ll count(ll x){//求小于等于x的数的个数
ll ans=x/d;
rep(i, 1, d-1){
if(x>=f[i]&&f[i]!=0)ans+=(x-f[i])/d+1;
}
return ans;
}
int main(){
ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
long long T;cin >> T;
while(T--){
long long inputS;cin >> inputS;S=inputS;
rep(i, 1, S-1){
if(S%i){
d=i;break;
}
}
rep(i, 1, d-1)f[i]=1e18, cho[i]=0, g[i]=1e18;
while(1){
ll Mi=1e18;
rep(x, 1, d-1)if(cho[x]==0){
ll res=0;
rep(i, 1, d-1){
res=max(res, (S-f[((S-i*x)%d+d)%d])/i);
}
res++;//必须大于等于res
ll remx=(res/d)*d+x;if(remx<res)remx+=d;
Mi=min(Mi, remx);
}
//选择把Mi加入
ll x=Mi%d;
if(Mi>=S)break;
cho[x]=1;
rep(i, 0, d-1)g[i]=f[i];
rep(i, 0, d-1){
rep(j, 1, d-1){
f[i]=min(f[i], g[((i-j*x)%d+d)%d]+j*Mi);
}
}
}
//f[i]就是模i剩余类中第一个被选中的
long long inputk;cin >> inputk;
ll k=inputk;
if(k>(S-1)/2){
cout << -1 << endl;
}else{
ll l=1, r=S-1, ans=0;
while(l<=r){
ll mid=(l+r)/2;
if(count(mid)>=k){
ans=mid;
r=mid-1;
}else{
l=mid+1;
}
}
cout << (long long)ans << endl;
}
}
return 0;
}

浙公网安备 33010602011771号