题解:P17300 [ICPC 2026 Xi'an I] Transform

· · 题解

题意简述

每次可以选出当前多重集的一个子多重集,将这些元素删除,再放入所选部分的 \operatorname{mex}。求将初始多重集变为 \{k\} 所需的最少操作次数。

解题思路

先考虑最后一次操作。为了生成 k,被选中的部分必须包含 0,1,\dots,k-1,且不能包含 k。因此,问题的关键是计算每个较小数字一共需要准备多少份。

c_i 为初始多重集中数字 i 的个数,d_i 为整个过程中生成数字 i 的操作次数。目标只含一个 k,所以最后必须且只需生成一次 k

d_k=1

每次生成某个大于 i 的数字,都要消耗至少一个 i。于是,所有更大数字对 i 的总需求为:

T_i=\sum_{j=i+1}^{k}d_j

数字 i 的来源只有初始的 c_i 份和操作生成的 d_i 份。要满足全部需求,必有:

c_i+d_i\ge T_i

若生成的 i 超过缺口,多出的操作不仅直接增加答案,还会继续消耗更小的数字,不可能使方案更优。因此最优取值为:

d_i=\max(0,T_i-c_i)

处理完数字 i 后,更小数字需要支撑原有的全部操作以及新增加的 d_i 次操作,所以:

T_{i-1}=T_i+\max(0,T_i-c_i)=\max(T_i,2T_i-c_i)

也就是说,当库存足够时需求不变;库存不足时,缺少的每一份都要额外生成。

初始时只有生成 k 的一次操作,因此 T_{k-1}=1。从 k-1 递推到 0 后:

T_{-1}=\sum_{i=0}^{k}d_i

这就是忽略初始数字 k 时的最少操作次数。

上述递推给出的不只是下界。可以按数字从小到大执行生成操作,并为每次生成较大数字预留一份所有更小数字。等式 c_i+d_i\ge T_i 保证每一层的库存都足够。没有被预留的重复元素可以并入某次删除,不会改变对应的 \operatorname{mex},所以递推值一定能够达到。

接下来处理初始多重集中的 k。若初始集合已经是 \{k\},不需要执行操作,答案为 0。否则,最后一次操作不能选中已有的 k。当递推答案至少为 2 时,可以在此前某次生成小于 k 的数字时顺便删除所有 k,因为大于当前 \operatorname{mex} 的元素不会影响结果。

若递推答案恰为 1,并且初始集合含有 k,就没有这样的前置操作。此时必须先增加一次操作删除已有的 k,再执行最终操作,故答案需要加一。

$$ T\gets 2^gT $$ 答案要对 $998244353$ 取模,但判断 $T_i\le c_i$ 必须使用真实大小。由于 $c_i\le n$,一旦 $T_i>n$,此后所有比较都会进入第二个分支。因此同时维护两个值:一个保存模意义下的答案,另一个把真实值截断为至多 $n+1$。 排序复杂度为 $O(n\log n)$。每个不同数字只进行一次递推和一次快速幂,空间复杂度为 $O(n)$。 ## 正确性证明 对任意 $i<k$,所有生成大于 $i$ 的操作都必须消耗一份 $i$,所以至少需要 $T_i$ 份 $i$。初始与生成的总量为 $c_i+d_i$,由此得到下界 $d_i\ge\max(0,T_i-c_i)$。取等号会同时最小化当前操作数和传给更小数字的需求,因此从大到小递推得到的是所有方案共同的下界。 按递推值生成每种数字时,关系 $c_i+d_i\ge T_i$ 保证能够把一份 $i$ 分配给每次需要它的更大数字生成操作。依次完成所有层后,可以生成一次 $k$。其余不参与后续生成的元素都能并入适当的删除集合而不改变 $\operatorname{mex}$,故这个下界可以达到。 若递推值至少为 $2$,在最终生成 $k$ 之前必有一次生成较小数字的操作,初始的所有 $k$ 都能在该操作中删除。若递推值为 $1$ 且初始含有 $k$,最终操作无法删除它,额外一次操作既是必要的,也足以先将其清除。结合初始集合已经为 $\{k\}$ 的特判,算法覆盖了全部情况,并输出最少操作次数。 ## 参考代码 ```cpp #include <bits/stdc++.h> using namespace std; using ll=long long; const int mod=998244353; struct node { ll val,res; }; ll Pow(ll x,ll y) { ll res=1; while(y) { if(y&1)res=res*x%mod; x=x*x%mod; y>>=1; } return res; } node jump(node f,int k,int n) { f.res=f.res*Pow(2,k)%mod; while(k&&f.val<=n) { f.val*=2; k--; } if(f.val>n)f.val=n+1; return f; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int T; cin>>T; while(T--) { int n,k; cin>>n>>k; vector<int>a; bool has=0; for(int i=1;i<=n;i++) { int x; cin>>x; if(x==k)has=1; if(x<k)a.push_back(x); } if(n==1&&has) { cout<<0<<'\n'; continue; } sort(a.begin(),a.end(),greater<int>()); node f={1,1}; int pos=k-1; for(int i=0;i<a.size();) { int j=i+1; while(j<a.size()&&a[j]==a[i])j++; f=jump(f,pos-a[i],n); int c=j-i; if(f.val>c) { f.val=min<ll>(n+1,2*f.val-c); f.res=(2*f.res-c+mod)%mod; } pos=a[i]-1; i=j; } f=jump(f,pos+1,n); if(has&&f.val==1)f.res=f.res+1==mod?0:f.res+1; cout<<f.res<<'\n'; } return 0; } ```