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;
}
posted @ 2025-06-26 10:53  yanzihe  阅读(10)  评论(0)    收藏  举报