题解:P10786 [NOI2024] 百万富翁

· · 题解

场外围观者来写一下 NOI 2024 D1T2 的题解。

话说为什么 D1T2 出交互题?为了让选手心态崩溃吗?

题目大意

一个长为 N 的数组 W_i,你可以发起 T 组共不超过 S 个询问,形如 W_iW_j 哪个更大。求出最大数。(原题面中的请求就是一组询问,下同)

测试点 1

询问组数只有 1,但个数\dfrac{N(N-1)}{2},那么对任意 1\le i\lt j\le N(i,j) 询问即可。比 N-1 个数大的那个数就是最大数。

测试点 2

思路

如果没有 $T$ 的限制,那么询问 $N-1$ 次就能得到最大数。 有了询问**组数**的限制后,应当确定一个思路:每组询问将**可能的最大数集合** $S$ **缩小**。初始 $S=\{1,2,\dots,N\}$,目标是在 $8$ 组询问后**将 $|S|$ 缩小至 $1$**。 由于交互库自适应,只有将一个集合中的数**两两做询问**,才能在**一组询问**中确定这个集合的最大数。 如果**一组询问中**希望将 $S$ 的大小除以 $2$,那么应当将 $S$ 分为 $\dfrac{|S|}{2}$ 组(关于可能的 $2\nmid|S|$ 情况暂不详细讨论),每组 $2$ 个数,之间做 $1$ 个询问。一共做 $\dfrac{|S|}{2}$ **个**询问。 类似地,如果希望将 $S$ 的大小除以 $k$,那么应当将 $S$ 分为 $\dfrac{|S|}{k}$ 组(关于可能的 $k\nmid|S|$ 情况也暂不讨论),每组 $k$ 个数,之间做 $\dfrac{k(k-1)}{2}$ 个询问。一共做 $\dfrac{|S|(k-1)}{2}$ 个询问。 可以将询问的**个数**当作**一组询问的代价**,那么为了让 $|S|$ 缩小更多,代价就更大。本题就是要在询问组数与代价之间**做出平衡**。 ### 求一个较好的代价与方案 注意到 $1099944$ 略大于 $N=10^6$,因此每次当然可以大胆地让 $|S|$ 除以 $2$,这样总询问次数为 $\dfrac{|S|}{2}+\dfrac{|S|}{4}+\dfrac{|S|}{8}+\dots$,在 $|S|$ 也就是 $N$ 左右。当然这是远远不够的。 手玩可以发现,前几组询问只能使 $|S|$ 除以 $2$;当 $|S|$ 缩小到 $10^4$ 级别时,多出的 $99944$ 个询问,就允许将 $|S|$ 除以更大的数(例如 $3,4,5,6$);并且,$|S|$ 大幅下降后,接下来每组询问的代价也会下降。 考虑设计 DP 求最小代价。采用倒推,设 $dp_{i,j}$ 表示**剩余 $j$ 次询问**,$|S|=i$ 时,将 $|S|$ 缩小为 $1$ 的代价(**其实这时不一定是最小代价,只是一种可行代价,让它尽量小**)。 初始 $dp_{i,1}=\dfrac{i(i-1)}{2}$。 枚举 $j=2,\dots,8$,枚举 $i$,再枚举 $k$ 表示希望让 $|S|$ 除以 $k$。此时钦定 $dp_{i,j}$ 从 $dp_{i',j-1}$ 转移,其中 $i'=\lceil\dfrac{i}{k}\rceil$。转移方程为 $dp_{i,j}=\min\{dp_{i,j},dp_{i',j-1}+\operatorname{calc}(i,k)\}$,其中 $\operatorname{calc}(i,k)$ 表示将 $S$ 分为 $i'=\lceil\dfrac{i}{k}\rceil$ 组,**每组之间两两询问**所需代价。 时间复杂度是 $O(N^2)$(因为 $i,k$ 均为从 $1$ 到 $N$)。其实可以优化,后面再说。 计算 $\operatorname{calc}(i,k)$ 时,设 $i=qk+r$,$0\le r\lt k$。 若 $r=0$ 则就是将 $S$ 的 $i$ 个元素分成 $q$ 组,一组 $k$ 个,代价 $\dfrac{qk(k-1)}{2}$。 若 $r\neq 0$,则 $i'=q+1$。考虑假想有 $i'$ 组,每组 $k$ 个数,则共有 $i'k=(q+1)k$ 个数,代价 $\dfrac{(q+1)k(k-1)}{2}$。但这样多了 $k-r$ 个数。若 $i'\ge k-r$,则从 $k-r$ 组中去除一个数即可,代价减少了 $(k-r)(k-1)$(因为每个多余的数和组内其余 $k-1$ 个数均不用询问了)。 若 $i'\lt k-r$ 怎么办?事实上,根本不需要担心,因为这种情况**可以忽略**。 手玩时还可以发现,第 $7$ 组询问之前,$|S|$ 很难下降到 $1000$ 及以下,那么第 $7$ 次询问的目标是将 $|S|$ 除以 $k_7$,$k_7$ 不会太大,一般 $k_7\le 30$。 于是,$i'\ge\dfrac{i}{k}=\dfrac{|S|}{k_7}\gt 33\gt k_7=k\gt k-r$。 实现时就是在 DP 中枚举 $i=1001,1002,\dots,N$,$j=2,3,\dots,30$。时间复杂度的问题也解决了,变成了 $O(N)$(常数巨大)。 ```cpp #include<bits/stdc++.h> using namespace std; #define N 1000000 #define INF 0x3f3f3f3f3f3f3fll #define ll long long ll dp[N+1][10],pre[N+1][10],v; inline ll calc(ll n,ll k){ static ll q,r;q=n/k,r=n%k; if(r==0)return q*(k*(k-1)/2); if(q+1>=k-r)return (q+1)*(k*(k-1)/2)-(k-r)*(k-1); return 0; } int main(){ for(ll i=1;i<=N;++i)dp[i][1]=i*(i-1)/2; for(ll i=2;i<=8;++i){ for(int j=1;j<=N;++j)dp[j][i]=INF; for(ll j=1001;j<=N;++j){ for(ll k=2;k<=30;++k){ v=dp[(j-1)/k+1][i-1]+calc(j,k); if(v<dp[j][i])dp[j][i]=v,pre[j][i]=k; } } } printf("%lld\n",dp[N][8]); int pos=N; for(int i=8;i>=2;--i){ printf("%d %lld (%lld)\n",pos,pre[pos][i],calc(pos,pre[pos][i])); pos=(pos-1)/pre[pos][i]+1; } printf("%d %d (%lld)\n",pos,pos,pos*(pos-1)/2); return 0; } ``` ### 优化方案 看一下 DP 的输出: ```text 1099947 1000000 2 (500000) 500000 2 (250000) 250000 2 (125000) 125000 2 (62500) 62500 3 (62498) 20834 6 (52075) 3473 19 (31221) 183 183 (16653) ``` 第一行是 $8$ 组询问的代价和,后面每一行为 $i,k,c$,代表 $S$ 集合的大小原本是 $i$,这组询问后它的大小除以了 $k$(上取整),代价为 $c$。 现在的总代价是 $1099947$,与目标 $1099944$ 只差了 $3$,接近正解了! 考察一下每组询问对 $|S|$ 的分法: 前四组,$|S|$ 除以了 $2$,分组时没有出现多余,不好优化。 第五组询问时,分法为 $62500=20832\times 3+2\times 2$。 第六组询问时,分法为 $20834=3469\times 6+4\times 5$。 第七组询问时,分法为 $3473=179\times 19+4\times 18$。 第八组询问只分了一组 $183=1\times 183$。 将第五组询问的 $2\times 2$ 合并为一组 $4$,代价增加了 $6-1-1=4$。 然而这样**第六组询问时 $|S|$ 减少了 $1$**,变为 $20833$。 将 $20833$ 分为 $3471\times 6+1\times 7$。这样的代价是 $52086$,比原来的 $52075$ 增加了 $11$。 但是这样**第七组询问的 $|S|$ 就减少了 $1$**。可以从大小为 $19$ 的组中减少一个数,这个数不再与组内的其余 $18$ 个数询问,代价减少了整整 $18$! 总代价变化为 $4+11-18=-3$,也就是减少了 $3$。这就得到了正解! ```cpp #include<bits/stdc++.h> #include "richest.h" //delete this in luogu.com.cn? #define n 1000009 int richest(int N,int T,int S); std::vector<int> ask(std::vector<int> a, std::vector<int> b); int siz[n]; std::vector<int> res,a,b,ret; std::vector<int> shrinker(std::vector<int> strategy,std::vector<int> lst){ a.clear();b.clear(); static int pos;pos=0; for(int i:strategy){ for(int j=pos;j<pos+i;++j)for(int k=pos;k<j;++k) a.push_back(lst[j]),b.push_back(lst[k]); pos+=i; } res=ask(a,b); for(int i:lst)siz[i]=0; ret.clear(); static int cnt;cnt=0;pos=0; for(int i:strategy){ for(int j=pos;j<pos+i;++j)for(int k=pos;k<j;++k)++siz[res[cnt]],++cnt; for(int j=pos;j<pos+i;++j)if(siz[lst[j]]==i-1)ret.push_back(lst[j]); pos+=i; } return ret; } std::vector<int> s,l; int richest(int N,int T,int S){ if(N==1000){ s.clear();s.push_back(N); l.clear();for(int i=0;i<N;++i)l.push_back(i); return shrinker(s,l)[0]; } else{//N==1000000 s.clear();for(int i=0;i<500000;++i)s.push_back(2); l.clear();for(int i=0;i<N;++i)l.push_back(i); l=shrinker(s,l); s.clear();for(int i=0;i<250000;++i)s.push_back(2); l=shrinker(s,l); s.clear();for(int i=0;i<125000;++i)s.push_back(2); l=shrinker(s,l); s.clear();for(int i=0;i<62500;++i)s.push_back(2); l=shrinker(s,l); s.clear();for(int i=0;i<20832;++i)s.push_back(3);s.push_back(4); l=shrinker(s,l); s.clear();for(int i=0;i<3471;++i)s.push_back(6);s.push_back(7); l=shrinker(s,l); s.clear();for(int i=0;i<178;++i)s.push_back(19);for(int i=0;i<5;++i)s.push_back(18); l=shrinker(s,l); s.clear();s.push_back(183); return shrinker(s,l)[0]; } } ``` 另外,以上的代码是在本题数据仍未上传时写的。为了验证正确性,我写了一个非适应性的 grader: ```cpp #include<bits/stdc++.h> int richest(int N,int T,int S); std::vector<int> ask(std::vector<int> a, std::vector<int> b); static int testid; static int N,T,S,W[1000009],ans,ret; static std::mt19937 mt(time(0)); inline int ri(int a,int b){return mt()%(b-a+1)+a;} static int m,t,s; static std::vector<int> res; std::vector<int> ask(std::vector<int> a, std::vector<int> b){ m=a.size(); if(m!=b.size()){ printf("#%d WA: Length of a and b are not the same.\n",testid); exit(1); } ++t;if(t>T){ printf("#%d WA: Too many requirements, get already %d/%d.\n",testid,t,T); exit(1); } s+=m;if(s>S){ printf("#%d WA: Too many queries, get already %d/%d.\n",testid,s,S); exit(1); } res.clear(); for(int i=0;i<m;++i){ if(W[a[i]]>W[b[i]])res.push_back(a[i]); else res.push_back(b[i]); } return res; } int main(){ std::scanf("%d%d%d",&N,&T,&S); for(testid=1;testid<=10;++testid){ t=s=0; for(int i=0;i<N;++i)W[i]=i; for(int i=0;i<N;++i)std::swap(W[i],W[ri(i,N-1)]); for(int i=0;i<N;++i)if(W[i]==N-1)ans=i; ret=richest(N,T,S); if(ret!=ans){ printf("#%d WA: Answer is %d, you returned %d.\n",testid,ans,ret); return 1; } printf("#%d AC: Correct.\n",testid); } return 0; } ```