P11393 [JOI Open 2019] 汇款 / Remittance 题解

· · 题解

模拟赛 T3(怎么大家都这样)。

赛时写挂了被捆绑测试阴了,赛后看题解发现我的思路怎么和大家都不一样 /ll。

怒调 1h 成功,写篇题解。

题意

N 座房子围成一圈,按逆时针方向用 1 \sim N 编号。初始第 i 座房子有 A_i 元,目标是达到 B_i 元。每座房子可以给它左边相邻的房子汇款,使得 A_i 减少 2xB_i 增加 x,需判断是否存在合法操作达成目标。

思路

题解区全是贪心,来个二分思路。

以下称一座房子从它右边的房子得到的钱为收入(扣除手续费后),向左边的房子汇款的钱为支出(未扣除手续费)。

定义 C_i=A_i-B_iin_i 为第 i 座房子的收入,out_i 为第 i 座房子的支出,那么有 C_i=out_i-in_i,out_i=2 \cdot in_{i+1}

该方程组为线性递推 + 环形约束,若存在可行解则一定唯一。

考虑二分第 1 座房子的收入 x,即 in_1=x

可以推出 out_n=2xout_1=C_1+x \Rightarrow in_2=\frac{out_1}{2}

in_2 继续向后推至 in_n=\frac{out_{n-1}}{2} 和理论 out'_n=C_n+in_n,此时判断理论 out_n 与之前计算出来的 out_n=2x 的大小关系:

::::info[单调性证明]{close} 令 \Delta(x)=out'_n-2x,证明 \Delta(x) 严格单调递减:

:::info[引理]{close}

数学归纳法证明:

基例 k=1in_1(x)=x=0+\frac{x}{2^0},即 P_1=0 成立。

设对 k=min_m(x)=P_m+\frac{x}{2^{m-1}}

k=m+1 时有:

out_m(x)=C_m+in_m(x)=C_m+P_m+\frac{x}{2^{m-1}} in_{m+1}(x)=\frac{out_m(x)}{2}=\frac{C_m+P_m}{2}+\frac{x}{2^m}

则令 P_{m+1}=\frac{C_m+P_m}{2},满足形式,归纳成立。 :::

k=n,代入引理结论:

in_n(x)=P_n+\frac{x}{2^{n-1}} out'_n(x)=C_n+in_n(x)=(C_n+P_n)+\frac{x}{2^{n-1}} \Delta(x)=out'_n(x)-2x=\underbrace{(C_n+P_n)}_{\text{Const}}+x \cdot (\frac{1}{2^{n-1}}-2) 题目保证 $n \ge 2$,故 $2^{n-1} \ge 2$,$\Delta(x)$ 斜率满足: $$ \frac{1}{2^{n-1}}-2 \le \frac{1}{2}-2 = -\frac{3}{2} < 0 $$ 斜率恒负,故 $\Delta(x)$ 严格单调递减。 :::: 写得比较丑陋 /kel。 ## 代码实现 123 行【】代码,谨慎食用。 :::success[Code]{close} ```cpp #include <iostream> #include <algorithm> #include <cmath> using namespace std; const double eps=1e-10;//防止精度误差 int n,a[1000001],b[1000001],nowin[1000001],nowout[1000001]; double c[1000001],in[1000001],out[1000001];//注:代码中out数组存储的是负数支出值 __int128 sum=0; bool cheek()//检验是否合法 { for(int i=1;i<=n;i++)//初始化 { nowin[i]=nowout[i]=0; } for(int i=1;i<=n;i++) { if(in[i]<0||out[i]>0||(in[i]!=0&&abs((int)in[i]-in[i])>eps)||(out[i]!=0&&abs((int)out[i]-out[i])>eps)||(int)out[i]%2!=0) return false; } bool can=false; while(1) { can=false;//本轮能否继续操作 for(int i=1;i<=n;i++) { if(nowout[i]>out[i]&&a[i]+nowin[i]+nowout[i]>0) { int k=min((int)(nowout[i]-out[i]),a[i]+nowin[i]+nowout[i])/2*2;//只支出偶数 nowout[i]-=k; nowin[i%n+1]+=k/2; if(k!=0) can=true; } } if(can==false) break; } for(int i=1;i<=n;i++)//再次遍历检验 { if(a[i]+nowin[i]+nowout[i]!=b[i]) { return false; } } return true; } /* check函数返回值: {true,true}满足条件,有正整数解 {true,false}满足条件,无正整数解 {false,false}不满足条件,大了 {false,true}不满足条件,小了 */ pair<bool,bool> check(int x) { in[1]=x; out[1]=-c[1]-x; out[n]=-2*x; for(int i=2;i<n;i++) { in[i]=out[i-1]/-2.0; out[i]=-c[i]-in[i]; } in[n]=out[n-1]/-2.0; double t=-c[n]-in[n]; if(t==out[n]) { if(cheek()) return {true,true}; else return {true,false}; } if(t<out[n]) return {false,true}; return {false,false}; } signed main() { ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>n; for(int i=1;i<=n;i++) { cin>>a[i]>>b[i]; c[i]=a[i]-b[i]; sum+=c[i]; } //一些特判 if(sum<0) { cout<<"No\n"; return 0; } if(sum==0) { for(int i=1;i<=n;i++) { if(c[i]!=0) { cout<<"No\n"; return 0; } } cout<<"Yes\n"; return 0; } long long l=0,r=2e9;//常数稍大 while(l<=r) { long long mid=(l+r)>>1; auto p=check(mid); if(p.first&&p.second) { cout<<"Yes\n"; return 0; } else if(p.first) { break; } else if(p.second) { l=mid+1; } else r=mid-1; } cout<<"No\n"; return 0; } ``` :::