P16422 Reorder 解题总结
\(\small \mathsf{date:20260925}\)
Part 1 数学推导
因为不停交换的贡献很难算,所以我们考虑将总贡献均摊到每一个选中的数上。
选 \(R\) 个位置移到最前面,设选中的数下标集合为 \(S=\{ p_1,p_2,\dots ,p_r\}\) ,且应保证 \(S\) 中元素有序。
若一个数被选中,设它为 \(p_k\) (即第 \(k\) 个选中的数) ,它需要被移动到前面。它前面原本有 \(p_k-1\) 个数,其中被选中的有 \(k-1\) 个。
因此,它本身交换贡献为 \(v_{p_k}\cdot (p_k-k)\) ,所有这样的数的贡献为 \(cost1=\sum_{k=1}^{r}v_{p_k}\cdot (p_k-k)\)。
若一个下标为 \(i\) 的数未被选中,设它的后面有 \(c_i\) 个被选中的数,所有没选中的数的下标集合为 \(T\)。
因此,所有没有被选中的数的贡献为 \(cost2=\sum_{i \in T}v_i\cdot c_i\) 。
其中,\(T\) 与 \(c\) 对“已经选了的那些数”均有依赖关系,考虑将它们消掉。
设 \(G\) 为 \(v\) 的前缀和数组,\(C\) 为选中的数的前缀和数组。
那么,\(cost2\) 就可以转化为 \(\sum_{k=1}^{R} G_{p_k-1}-C_{p_k-1}\)。
感性理解的话,就是将全部的“挖掉”选中的,系数 \(c\) 转化为前缀和的重复计算。
加上最终前 \(R\) 个的费用,得
发现 \(C\) 对 \(S\) 仍然有依赖,因此拆掉它:
带回原式,\(totalcost=\sum_{k=1}^{R}[v_{p_k}(p_k-R+A)+G_{p_k-1}]\) ,赏心悦目。
所以,均摊到每一项上,\(cost_i(R)=-v_i\cdot R+[v_i(A+i)+G_{i-1}]\) 。
至此,此题完工 \(30\%\) 。
Part 2 数据结构
括号里对固定的 \(i\) 是关于 \(R\) 的一次函数。设 \(f_i(x)=k_ix+b_i\),其中 \(k_i=-v_i\),\(b_i=v_i i-P_i+V+Av_i\)。那么问题就变成:对每个 \(R\),从 \(n\) 条直线里选 \(R\) 条,使它们在 \(x=R\) 处的值之和最小,也就是求 \(f_i(R)\) 的前 \(R\) 小之和,即 \(x=R\) 与这些直线的从下往上前 \(R\) 个交点纵坐标之和。
后面两个含 \(c_i\) 的部分合起来正好是 \(\sum_{i=1}^n v_ic_i\)。这个量直接看不好算,但换个角度:\(c_i\) 是 \(i\) 前面被选中的个数,所以 \(\sum v_ic_i\) 等于每个被选中的位置 \(j\) 对它后面所有数的 \(v\) 之和的贡献。记 \(P_i=\sum_{j=i}^n v_j\),那么 \(\sum_{i=1}^n v_ic_i=\sum_{i\in S}P_i\)。再记 \(V=\sum v_i\),把 \(R\sum_{i\notin S}v_i\) 用 \(RV-R\sum_{i\in S}v_i\) 替换,再加上移动费用 \(Av_i\),得到:
括号里对固定的 \(i\) 是关于 \(R\) 的一次函数。设 \(f_i(x)=k_ix+b_i\),其中 \(k_i=-v_i\),\(b_i=v_i i-P_i+V+Av_i\)。那么问题就变成:对每个 \(R\),从 \(n\) 条直线里选 \(R\) 条,使它们在 \(x=R\) 处的值之和最小,也就是求 \(f_i(R)\) 的前 \(R\) 小之和。
前 \(R\) 小的和没法直接维护,但 \(R\) 是从 \(1\) 到 \(n\) 递增的,可以增量做。每次 \(R\) 增加 \(1\),就多选一条线。新选的显然是当前未选线中在 \(x=R\) 处最小的那条。但选完之后可能原来选中的某条线变大了,比未选中的某条还大,这时交换它们更优。于是维护两个集合:已选集合 \(S\) 和未选集合 \(\bar S\)。每次先把 \(\bar S\) 的最小值加进 \(S\),然后只要 \(\max(S)>\min(\bar S)\) 就交换,直到不满足为止。这样 \(S\) 就恰好是前 \(R\) 小。
暴力做的问题是交换次数。假设某条线被换了 \(w_i\) 次,总交换次数是 \(\sum w_i^2\),最坏 \(O(n^2)\)。解决办法是分块。按斜率排序后切成 \(\sqrt n\) 大小的块,每块维护两个凸包:未选部分的下凸包用来查最小值,已选部分的上凸包用来查最大值。块内查询 \(O(\log n)\),重建一块 \(O(\sqrt n)\)。
每块对外只需要给出两个值:块内未选最小、块内已选最大。把所有块丢进两个堆,小根堆取未选最小,大根堆取已选最大。块重建后堆里会有旧元素,用版本号懒删除:每块有个 \(ver\),重建时加一,堆节点记入堆时的 \(ver\),取堆顶时不对就弹掉。
因为 \(R\) 递增,查询的 \(x\) 越来越大,凸包上的最优点只会往右移,不会回退,所以凸包指针可以一直右移,均摊 \(O(1)\)。
另外,由于出题人的毒瘤,这个东西需要特判:\(Q=1,n\le 10^6\) 。
具体方法:如果只有一个询问,没必要预处理 \(1\) 到 \(n\),直接算所有 \(f_i(R)\) 然后 nth_element 取前 \(R\) 小,\(O(n)\) 就完事了。
Part 3 参考程序
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef __int128_t i128;
struct Line
{
ll k,b;
int id;
ll cal(ll x) const{return k*x+b;}
};
struct Hull
{
vector<int> q;
int p;
Hull():p(0){}
void clr()
{
q.clear();
p=0;
}
};
struct Blk
{
vector<int> ids;
Hull mn,mx;
int mni,mxi;
ll mnv,mxv;
int ver;
Blk():mni(-1),mxi(-1),mnv(0),mxv(0),ver(0){}
};
struct HNode
{
ll v;
int blk,id,ver;
};
struct CmpMn
{
bool operator()(const HNode &a,const HNode &b) const
{
if(a.v!=b.v) return a.v>b.v;
return a.id>b.id;
}
};
struct CmpMx
{
bool operator()(const HNode &a,const HNode &b) const
{
if(a.v!=b.v) return a.v<b.v;
return a.id>b.id;
}
};
int n,q;
ll A;
vector<ll> v,pre;
vector<int> ask;
vector<Line> ln;
vector<int> ord;
vector<Blk> blk;
vector<int> bel;
vector<char> sel;
bool bad(int a,int b,int c,bool neg)
{
ll ka=neg?-ln[a].k:ln[a].k;
ll ba=neg?-ln[a].b:ln[a].b;
ll kb=neg?-ln[b].k:ln[b].k;
ll bb=neg?-ln[b].b:ln[b].b;
ll kc=neg?-ln[c].k:ln[c].k;
ll bc=neg?-ln[c].b:ln[c].b;
ll n1=bb-ba,d1=ka-kb;
ll n2=bc-bb,d2=kb-kc;
if(d1<0)
{
d1=-d1;
n1=-n1;
}
if(d2<0)
{
d2=-d2;
n2=-n2;
}
return (i128)n1*d2>=(i128)n2*d1;
}
void rbd(int b,ll x)
{
Blk &B=blk[b];
B.mn.clr();
for(int i=0;i<(int)B.ids.size();i++)
{
int id=B.ids[i];
if(sel[id]) continue;
if(!B.mn.q.empty()&&ln[B.mn.q.back()].k==ln[id].k)
{
if(ln[id].b<ln[B.mn.q.back()].b) B.mn.q.back()=id;
continue;
}
while(B.mn.q.size()>=2&&bad(B.mn.q[B.mn.q.size()-2],B.mn.q.back(),id,false)) B.mn.q.pop_back();
B.mn.q.push_back(id);
}
while(B.mn.p+1<(int)B.mn.q.size()&&ln[B.mn.q[B.mn.p+1]].cal(x)<=ln[B.mn.q[B.mn.p]].cal(x)) B.mn.p++;
if(B.mn.q.empty())
{
B.mni=-1;
B.mnv=0;
}
else
{
B.mni=B.mn.q[B.mn.p];
B.mnv=ln[B.mni].cal(x);
}
B.mx.clr();
for(int i=(int)B.ids.size()-1;i>=0;i--)
{
int id=B.ids[i];
if(!sel[id]) continue;
if(!B.mx.q.empty()&&ln[B.mx.q.back()].k==ln[id].k)
{
if(ln[id].b>ln[B.mx.q.back()].b) B.mx.q.back()=id;
continue;
}
while(B.mx.q.size()>=2&&bad(B.mx.q[B.mx.q.size()-2],B.mx.q.back(),id,true)) B.mx.q.pop_back();
B.mx.q.push_back(id);
}
while(B.mx.p+1<(int)B.mx.q.size()&&ln[B.mx.q[B.mx.p+1]].cal(x)>=ln[B.mx.q[B.mx.p]].cal(x)) B.mx.p++;
if(B.mx.q.empty())
{
B.mxi=-1;
B.mxv=0;
}
else
{
B.mxi=B.mx.q[B.mx.p];
B.mxv=ln[B.mxi].cal(x);
}
B.ver++;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>q>>A;
v.resize(n+1);
pre.resize(n+1);
for(int i=1;i<=n;i++)
{
cin>>v[i];
pre[i]=pre[i-1]+v[i];
}
ask.resize(q);
for(int i=0;i<q;i++) cin>>ask[i];
ln.resize(n+1);
for(int i=1;i<=n;i++)
{
ln[i].k=-v[i];
ln[i].b=v[i]*(A+(ll)i)+pre[i-1];
ln[i].id=i;
}
if(q==1)
{
int R=ask[0];
vector<ll> tmp(n);
for(int i=1;i<=n;i++) tmp[i-1]=ln[i].cal(R);
nth_element(tmp.begin(),tmp.begin()+R-1,tmp.end());
i128 ans=0;
for(int i=0;i<R;i++) ans+=tmp[i];
cout<<(long long)ans<<"\n";
return 0;
}
ord.resize(n);
for(int i=0;i<n;i++) ord[i]=i+1;
sort(ord.begin(),ord.end(),[](int x,int y)
{
if(ln[x].k!=ln[y].k) return ln[x].k>ln[y].k;
return x<y;
});
int B=max(1,(int)sqrt(n)+1);
int m=(n+B-1)/B;
blk.resize(m);
bel.resize(n+1);
for(int b=0;b<m;b++)
{
int L=b*B;
int R=min(n,(b+1)*B);
for(int i=L;i<R;i++) blk[b].ids.push_back(ord[i]);
for(int i=0;i<(int)blk[b].ids.size();i++) bel[blk[b].ids[i]]=b;
}
sel.assign(n+1,0);
for(int b=0;b<m;b++) rbd(b,1);
ll sm=0,sl=0;
vector<ll> ans(n+1);
for(int R=1;R<=n;R++)
{
sm+=sl;
for(int b=0;b<m;b++)
{
Blk &B=blk[b];
if(!B.mn.q.empty())
{
while(B.mn.p+1<(int)B.mn.q.size()&&ln[B.mn.q[B.mn.p+1]].cal(R)<=ln[B.mn.q[B.mn.p]].cal(R)) B.mn.p++;
B.mni=B.mn.q[B.mn.p];
B.mnv=ln[B.mni].cal(R);
}
if(!B.mx.q.empty())
{
while(B.mx.p+1<(int)B.mx.q.size()&&ln[B.mx.q[B.mx.p+1]].cal(R)>=ln[B.mx.q[B.mx.p]].cal(R)) B.mx.p++;
B.mxi=B.mx.q[B.mx.p];
B.mxv=ln[B.mxi].cal(R);
}
}
priority_queue<HNode,vector<HNode>,CmpMn> qmn;
priority_queue<HNode,vector<HNode>,CmpMx> qmx;
for(int b=0;b<m;b++)
{
Blk &B=blk[b];
if(B.mni!=-1) qmn.push({B.mnv,b,B.mni,B.ver});
if(B.mxi!=-1) qmx.push({B.mxv,b,B.mxi,B.ver});
}
while(!qmn.empty())
{
HNode e=qmn.top();
Blk &B=blk[e.blk];
if(e.ver==B.ver&&e.id==B.mni&&e.v==B.mnv) break;
qmn.pop();
}
while(!qmx.empty())
{
HNode e=qmx.top();
Blk &B=blk[e.blk];
if(e.ver==B.ver&&e.id==B.mxi&&e.v==B.mxv) break;
qmx.pop();
}
int inId=qmn.top().id;
ll inV=ln[inId].cal(R);
sel[inId]=1;
sm+=inV;
sl+=ln[inId].k;
int bin=bel[inId];
rbd(bin,R);
{
Blk &B=blk[bin];
if(B.mni!=-1) qmn.push({B.mnv,bin,B.mni,B.ver});
if(B.mxi!=-1) qmx.push({B.mxv,bin,B.mxi,B.ver});
}
while(true)
{
while(!qmn.empty())
{
HNode e=qmn.top();
Blk &B=blk[e.blk];
if(e.ver==B.ver&&e.id==B.mni&&e.v==B.mnv) break;
qmn.pop();
}
while(!qmx.empty())
{
HNode e=qmx.top();
Blk &B=blk[e.blk];
if(e.ver==B.ver&&e.id==B.mxi&&e.v==B.mxv) break;
qmx.pop();
}
if(qmn.empty()||qmx.empty()) break;
HNode mn=qmn.top();
HNode mx=qmx.top();
if(mx.v<=mn.v) break;
int addId=mn.id,delId=mx.id;
ll addV=ln[addId].cal(R);
ll delV=ln[delId].cal(R);
int ba=bel[addId],bd=bel[delId];
sel[addId]=1;
sel[delId]=0;
sm+=addV-delV;
sl+=ln[addId].k-ln[delId].k;
rbd(ba,R);
{
Blk &B=blk[ba];
if(B.mni!=-1) qmn.push({B.mnv,ba,B.mni,B.ver});
if(B.mxi!=-1) qmx.push({B.mxv,ba,B.mxi,B.ver});
}
if(ba!=bd)
{
rbd(bd,R);
Blk &B=blk[bd];
if(B.mni!=-1) qmn.push({B.mnv,bd,B.mni,B.ver});
if(B.mxi!=-1) qmx.push({B.mxv,bd,B.mxi,B.ver});
}
}
ans[R]=sm;
}
for(int i=0;i<q;i++) cout<<ans[ask[i]]<<"\n";
return 0;
}
复杂度 \(O(n\sqrt n\log n)\)。\(q=1\) 时特判后 \(O(n)\)。因为凸包指针随 \(R\) 单调移动,块内查询均摊 \(O(1)\),实际跑起来常数不大。

浙公网安备 33010602011771号