CXXXII.[GYM102268J]Jealous Split
wqs二分。
首先,先讲一下wqs二分的应用条件:
对于某个函数 \(f(x)\) 和一个特定的 \(x\),要求出 \(f(x)\) 的值的复杂度是不可接受的;但是,若满足 \(f\) 是上凸/下凹的,且对于一个给定的 \(k\),求出函数 \(f(x)+xk\) 的全局最大(也可能是小,视 \(f\) 的凹凸性而定)值及取得最值的位置的复杂度是可接受的,则此时就可以使用此种方式来求解。
具体来说,明显 \(f(x)\) 会在平面上构成一个凸壳;而对于一个给定的 \(k\) 求全局的最值,就相当于拿一条斜率为 \(-k\) 的直线去切凸壳,最值出现的位置就是切点的位置。
明显我们可以二分 \(k\),即二分切线斜率。显然,通过二分,最后定会切到给定的 \(x\) 的位置,则此时我们就已经知道了 \(f(x)+xk\) 的值及 \(k\) 的值,倒着推一下就能推回 \(f(x)\) 出来。
但需要注意的是,对于某些 \(k\),可能其与凸壳的切点不止一个,但因为其是凸的,故所有切点必定是一段区间。这时候就只需判断要求的 \(x\) 是在区间左边、内部或者右边即可。假如在区间内部,就可以直接返回了。
我们回到本题。
先从简单的情形想起:假如仅能切一刀,应该怎么切?
明显我们可以初始令最左方的元素单独成段,然后剩下元素再成一段。之后,不断向右移动切点,直到再往右移动会使得左段元素和大于右端元素和。这时,明显一定满足题目要求,因为当且仅当切点将要跨越的那个元素比当前的差要大才会停止移动,而这本身就表明已经有一个元素大于当前差的绝对值。
当能切更多刀时,明显我们可以通过逐步调整法使得每一刀都符合上述要求,也即解一定存在。
我们发现,这种情形下,对于任意实数 \(k>1\),都应有所有(段内元素之和的 \(k\) 次幂)之和最小。取 \(k=2\) 会使计算简便。
于是我们现在就要找到一种分成 \(m\) 段且所有段的平方和最小。显然直接DP是 \(O(n^2)\) 的。
但是,我们发现平方和,随着段数的越分越多,肯定是递减的;不仅如此,其还是下凸的,因为否则我们一定可以更早地分开一段使答案更优。
考虑套上wqs二分。于是我们就变成了每切一段需要额外的代价 \(k\),求平方和加上额外代价最小的分段方式。这是简单斜率优化,可以 \(O(n)\) 使用单调队列简单解决。
但是依据我们上文对wqs二分的分析,其好像并不能很好地求出方案来(因为不能保证切线刚好就切到我们需要的点上)。事实上,对于大多数的wqs二分题,我们都无法找到一种方案。
但是,注意到这里说的是大多数!事实上,对于本题这种“区间划分”wqs二分问题,的确是有一种构造方案的。具体而言,当求出的区间包含目标位置时,我们可以求出分段最少的方法以及最多的方法——可以通过斜率优化时当队首首个和次个元素的斜率与所需斜率相等时,出不出队来分别求出。明显我们的目标介于两种方法之间。
考虑用一种方法的一段前缀与另一种方法的一段后缀拼成完整的分段方案。考虑如何拼接。
首先,若两者方法出现了相同的分割点,明显在分割点处拼接前一半和后一半是一种合法的方案,因为最小方法和最大方法本质相当于从初始状态到终止状态的两条不同的最短路,一条从起点到终点的最短路,对于路径上所有节点,也必是最短路,故拼接合法。
我们还有一种拼接方法。考虑最少方案中,出现了一段区间 \([l,r]\);最多方案中,出现了一段区间 \([L,R]\);若有 \(l<L\land R<r\),则我们可以用最少方案中 \(l\) 以前的前缀,拼上最多方案中 \(R\) 以后的后缀构成一组方案,也可以用 \(L\) 以前和 \(r\) 以后拼成另一种方案。具体的话,因为区间和的平方函数满足四边形不等式,所以新方案一定不更劣;但因为其本身都是最短路,故新方案最优也只能是最短路,所以两者结合,就得到新方案必定是最短路。
可以被证明的是,一定存在一种拼接方案满足长度恰好为任何 \(k\in[L,R]\),其中 \(L,R\) 分别为最少方案、最多方案的长度。因为依据鸽巢原理,最多方案中一定至少有 \(R-L\) 个区间是最少方案中某个区间的子区间;而我们构造的一种方案中包含多少个上述区间,其长度就会在 \(L\) 基础上增加多少。明显我们上述方案中包含了从 \(0\) 个到 \(R-L\) 个所有可能需要的个数的构造方式,故我们一定可以构造出符合条件的方式。
于是我们现在已经知道如何二分、如何构造方式了。是不是就结束了呢?
稍等!我们还没有讨论 \(0\) 的情形——\(0\) 会使得问题变麻烦,因为斜率优化时前缀和就不是严格单调递增了。一个明智的想法是忽略所有 \(0\),直到方式构造完成后再加入 \(0\)。
然后就是一些具体实现的问题了。一开始我wqs二分写的是实数二分,因为考虑到斜率可能为一切实数。但是后来思考发现,只需整数二分,得到的直线就可以切到所有点,于是就换成了整数二分。
实际上,还有一个原因是本题数据范围就非常尴尬(序列中所有数之和最大为 \(50\) 亿,刚好爆 int
,平方后就爆了 long long
),即使使用long double
,也会被卡精度 WA 71
。用实数二分就无法避免地用 long double
存状态,因此会挂。
但是爆 long long
也意为着我们不能使用 long long
储存。我尝试使用了压 13
个 bit
的 \(8\) 个 int
来模拟 __int128
,但是被卡常了。所以最后索性直接上了 __int128
(第一次知道 CF 开 C++17(64)
就能用 __int128
的),才卡过。
代码:
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128 ii;
int n,m,F[100100],G[100100];
ii f[100100],g[100100];
ll s[100100];
ii sqr(ll x){return (ii)x*x;}
int read(){
int x=0;
char c=getchar();
while(c>'9'||c<'0')c=getchar();
while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x;
}
int q[100100],l,r;
int che(ll ip){
// printf("%lld:\n",ip);
l=r=0;
for(int i=1;i<=n;i++){
if(s[i]==s[i-1]){f[i]=f[i-1],F[i]=F[i-1];continue;}
// if(r-l>=1)(2*BigNum(s[i])*(s[q[l+1]]-s[q[l]])).print(),(f[q[l+1]]-f[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])).print(),puts("");
while(r-l>=1&&f[q[l+1]]-f[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])<(ii)2*s[i]*(s[q[l+1]]-s[q[l]]))l++;
F[i]=q[l],f[i]=f[q[l]]+sqr(s[i]-s[q[l]])+ip;
while(r-l>=1&&(f[q[r]]-f[q[r-1]]+sqr(s[q[r]])-sqr(s[q[r-1]]))*(s[i]-s[q[r]])>(f[i]-f[q[r]]+sqr(s[i])-sqr(s[q[r]]))*(s[q[r]]-s[q[r-1]]))r--;
q[++r]=i;
}
// for(int i=1;i<=n;i++)f[i].print();puts("");
// for(int i=1;i<=n;i++)printf("%d ",F[i]);puts("");
l=r=0;
for(int i=1;i<=n;i++){
if(s[i]==s[i-1]){g[i]=g[i-1],G[i]=G[i-1];continue;}
// if(r-l>=1)printf("%d:%d,%d\n",i,q[l],q[l+1]),(2*BigNum(s[i])*(s[q[l+1]]-s[q[l]])).print(),(g[q[l+1]]-g[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])).print(),puts("");
while(r-l>=1&&g[q[l+1]]-g[q[l]]+sqr(s[q[l+1]])-sqr(s[q[l]])<=(ii)2*s[i]*(s[q[l+1]]-s[q[l]]))l++;
G[i]=q[l],g[i]=g[q[l]]+sqr(s[i]-s[q[l]])+ip;
while(r-l>=1&&(g[q[r]]-g[q[r-1]]+sqr(s[q[r]])-sqr(s[q[r-1]]))*(s[i]-s[q[r]])>=(g[i]-g[q[r]]+sqr(s[i])-sqr(s[q[r]]))*(s[q[r]]-s[q[r-1]]))r--;
q[++r]=i;
}
// for(int i=1;i<=n;i++)g[i].print();puts("");
// for(int i=1;i<=n;i++)printf("%d ",G[i]);puts("");
l=0;for(int i=n;i;i=F[i])l++;
r=0;for(int i=n;i;i=G[i])r++;
// printf("[%d %d]\n",l,r);
if(l>m)return -1;
if(r<m)return 1;
return 0;
}
vector<int>u,v;
int tot;
int main(){
n=read(),m=read();
for(int i=1;i<=n;i++)s[i]=s[i-1]+read(),tot+=(s[i]!=s[i-1]);
puts("Yes");
if(tot<m){//cases when there have to be single 0 sections
for(int i=1;i<n;i++){
if(tot<m&&s[i]==s[i-1])tot++,printf("%d ",i);
if(s[i]!=s[i-1])printf("%d ",i);
}puts("");return 0;
}
// for(int i=1;i<=n;i++)printf("%Lf ",s[i]);puts("");
ll L=0,R=0x3f3f3f3f3f3f3f3f,mid;
while(true){
// printf("%lld %lld\n",L,R);
int tmp=che(mid=(L+R)>>1);
if(tmp==-1)L=mid+1;
if(tmp==1)R=mid-1;
if(!tmp)break;
}
// printf("%d %d\n",l,r);
for(int i=n;i;i=F[i])u.push_back(i);u.push_back(0),reverse(u.begin(),u.end());
for(int i=n;i;i=G[i])v.push_back(i);v.push_back(0),reverse(v.begin(),v.end());
// for(auto i:u)printf("%d ",i);puts("");
// for(auto i:v)printf("%d ",i);puts("");
// if(l==m){for(int i=1;i+1<u.size();i++)printf("%d ",u[i]);puts("");return 0;}
// if(r==m){for(int i=1;i+1<v.size();i++)printf("%d ",v[i]);puts("");return 0;}
for(int i=1,j=0;i<u.size();i++){
for(;j<v.size()&&v[j]<u[i];j++){
if(!j||v[j-1]<=u[i-1])continue;
if(i+v.size()-j-1==m){for(int k=1;k<i;k++)printf("%d ",u[k]);for(int k=j;k+1<v.size();k++)printf("%d ",v[k]);puts("");return 0;}
if(j+u.size()-i-1==m){for(int k=1;k<j;k++)printf("%d ",v[k]);for(int k=i;k+1<u.size();k++)printf("%d ",u[k]);puts("");return 0;}
}
if(j==v.size()||v[j]!=u[i])continue;
if(i+v.size()-j-1==m){for(int k=1;k<i;k++)printf("%d ",u[k]);for(int k=j;k+1<v.size();k++)printf("%d ",v[k]);puts("");return 0;}
if(j+u.size()-i-1==m){for(int k=1;k<j;k++)printf("%d ",v[k]);for(int k=i;k+1<u.size();k++)printf("%d ",u[k]);puts("");return 0;}
}
return 0;
}