QOJ53. Hanoi Sort & P10327 [UESTCPC 2024] 汉诺塔排序问题 题解
本文参考了 这篇题解 和 QOJ 官方题解,同时对于两篇题解表述不清的地方做出了补充。核心补充为:\(pos\) 和 \(optpos\) 是分界线,题解暗含了反悔贪心的思想。
考虑将整个过程倒过来:一开始 \(B\) 从下往上是 \(n\sim 1\),把它按顺序加入 \(A\) 中。
放入 \(A\) 的可以不用管了,看作删除。
可以把 \(B,C\) 顶部的一些数归并,倒着放入 \(A\),然后一起放入 \(B\) 或 \(C\)。最大的那个数可以省一次操作。假设归并了 \(k\) 个数,代价是 \(2k-1\)。
把 \(p_i\) 移到 \(A\),假设 \(p_i\) 现在在 \(B\),需要把 \(B\) 中 \(p_i\) 上面的数,以及 \(C\) 的一个前缀——可以看作剩下的数(\(B,C\) 归并后的数组,从小到大排列)的一个前缀——归并。
接着就是 原题题解 的过程。\(pos,optpos\) 记录的是分界线,而不是点,分界线 \(i\) 表示分割 \(i\) 和 \(i+1\)(值域从大往小)。
用一个操作序列栈维护分界线,分界线把序列(\(B,C\) 归并后的数组,从上往下值域递减)分成若干段,越接近栈顶的分界线在序列里越靠上。每一条分界线表示了一个操作(把某个 \(a_j\) 移到 \(A\) 涉及的过程),影响的是分界线(在序列)上方的数;最下面一条分界线下方的数没有被操作过;越靠下的分界线对应的操作越老;新的操作可能把旧的操作覆盖掉(见下文)。分界线有三个变量:\(\{pos,optpos,diff\}\)。
- \(pos\) 表示分界线的位置,它把序列排名(按值域从大到小排,值最大的排名为 \(1\))为 \(pos\) 和 \(pos+1\) 的分割。
- \(optpos\) 表示分界线对应的操作中,把它上方的数进行了归并,让它们位于同一个栈(栈 \(B\) 或栈 \(C\))里。注意 \(optpos\) 依然是一条分界线,分界线的编号是它下面还剩多少个数,定义和 \(pos\) 一样。
- \(diff\) 表示分界线 \(pos\) 的上下两个数(值域从大到小排,排名为 \(pos\) 和 \(pos+1\) 的)是否在不同的栈里。
现在假设要把 \(a_i\) 移到 \(A\)(然后视作被删除),有 \(pos\) 个 \(\ge a_i\) 的数没有被删除。现在的目标是把分界线 \(pos\) 上方的数全都移到和 \(a_i\) 不同的栈里。
定义一个数的“颜色”为它所在栈的编号,“颜色”只有 \(B\) 或 \(C\)。一次归并可以看作把序列(\(B,C\) 归并后的数组)顶端的一些数的颜色推平(设置为同一个值)。
设 \(m=n-i+1\) 表示剩下的数,设 \(optpos:=\infin\) 表示这一轮操作会把分界线 \(optpos\) 上方的数归并,\(cost:=0\) 表示代价。接下来考虑如下流程。
- 弹操作序列栈。假设栈顶的分界线为 \(nd\),如果满足 \(nd.pos>pos\) 就弹栈,并进行如下操作:
- 如果 \(nd.diff=true\),需要把“上一条分界线”下方到 \(nd\) 上方的颜色(所在栈编号,\(B\) 或 \(C\))推平。要么更改以前的操作,贡献为 \(cost:=cost+2\times(\min\{optpos,nd.optpos\}-nd.pos)\);要么把操作重新设置为 \(nd.pos\),贡献为 \(cost:=\max\{0,2\times(m-nd.pos)-1\}\)。两种贡献去 \(\min\) 即可。无论如何,新的 \(optpos\) 都会被设置为 \(nd.pos\)。
- 如果 \(nd.diff=false\),可以不用管它,但这个操作的分界线可能更靠下,因此需要,\(optpos:=\min\{optpos,nd.optpos\}\)。
- 弹完栈以后,如果栈顶满足 \(nd.pos=pos\) 再次弹栈,此时需要使得它和下方的颜色不同,操作和步骤 \(1\) 相反:
- 如果 \(nd.diff=true\),那么 \(optpos:=\min\{optpos,nd.optpot\}\)。
- 如果 \(nd.diff=false\),那么 \(cost:=\min\{cost+2\times(\min\{optpos,nd.optpos\}-nd.pos),\max\{0,2\times(m-nd.pos)-1\}\}\),并且 \(optpos:=nd.pos\)。
- 如果步骤 \(2\) 的判断不成立,即做完步骤 \(1\) 后栈为空或者 \(nd.pos<pos\),那么还有一段需要修改:
- \(cost:=\min\{cost+2\times(optpos-nd.pos),\max\{0,2\times(m-nd.pos)-1\}\}\),\(optpos:=pos\)。
- 做完上面的所有,新加入代表这个操作的分界线,\(\{pos-1,optpos,true\}\)。需要注意的是,如果此时栈顶已经满足 \(nd.pos=pos-1\) 了,就无需新加入分界线,而是更新栈顶的分界线,\(nd.pos:=\min\{nd.optpos,optpos\}\),\(nd.diff=\neg nd.diff\)。
答案是 \(n\) 加上每一次的 \(cost\)。
但依然存在疑点。步骤 \(1\) 和步骤 \(2\) 的 \(cost:=cost+2\times(\min\{optpos,nd.optpos\}-nd.pos)\) 具体对应的是什么操作?这里类似反悔贪心,把之前步骤的 \(optpos\) 进行了更改。类似的,\(optpos:=\min\{optpos,nd.optpos\}\) 成功 ckmin 后,设 \(nd.optpos\) 对应的操作为第 \(j\) 个操作(删除 \(a_j\) 的操作),如果第一个式子的 \(\min\) 取 \(optpos\),表示把第 \(j\) 个操作的 \(optpos\) 进行修改。第 \(j\) 个 \(optpos\) 可能在 \(a_j\) 的下方,此时 \(a_j\) 也会被归并,相当于多倒腾两步(假设当时 \(a_j\) 在栈 \(B\) 中,和别的元素一样先放入 \(A\),只不过别的元素放入 \(C\),而 \(a_j\) 放入 \(B\),具体可见 这篇题解 的“Hack”部分)。注意到步骤 \(4\) 加入的 \(optpos\) 表示它下方有多少个元素,这里会把 \(a_i\) 算上,因此虽然 \(a_i\) 被删除了,但反悔的时候贡献依然会被算上。因此这里的 \(optpos\) 可以看作额外带了一个指针 \(t\),表示这个 \(optpos\) 是删除 \(a_t\) 时的 \(optpos\)。
:::info[代码]
const int N=100099,inf=1e9;
int T,n,p[N]={},tr[N]={};
void add(int x,int v) { while(x) tr[x]+=v,x-=x&-x; return ; }
int get(int x) { int rey=0; while(x<=n) rey+=tr[x],x+=x&-x; return rey; }
struct note {
int pos,optpos,diff;
note()=default;
note(int a,int b,int c):pos(a),optpos(b),diff(c){}
} zhan[N]={}; int top=0;
int main()
{
int i; ll ans=0;
read(T);
for(int _=1;_<=T;++_) {
read(n);
for(i=1;i<=n;++i) read(p[i]);
memset(tr,0,(n+1)<<2);
for(i=1;i<=n;++i) add(i,1);
top=0;
ans=0;
for(i=1;i<=n;++i) {
int m=n-i+1,optpos=inf,cost=0,pos=get(p[i]);
add(p[i],-1);
while(top&&zhan[top].pos>pos) {
note nd=zhan[top];
--top;
if(nd.diff) {
cost=min(cost+2*(min(optpos,nd.optpos)-nd.pos),max(0,2*(m-nd.pos)-1));
optpos=nd.pos;
} else {
ckmin(optpos,nd.optpos);
}
}
if(top&&zhan[top].pos==pos) {
note nd=zhan[top];
--top;
if(nd.diff) ckmin(optpos,nd.optpos);
else cost=min(cost+2*(min(optpos,nd.optpos)-nd.pos),max(0,2*(m-nd.pos)-1)),optpos=nd.pos;
} else {
cost=min(cost+2*(optpos-pos),max(0,2*(m-pos)-1));
optpos=pos;
}
ans+=cost;
if(top&&zhan[top].pos==pos-1) {
ckmin(zhan[top].optpos,optpos);
zhan[top].diff^=1;
} else {
zhan[++top]=note(pos-1,optpos,true);
}
}
printf("%lld\n",ans+n);
}
// cerr<<"Use time: "<<(double)clock()/CLOCKS_PER_SEC*1000.0<<"ms"<<endl;
return 0;
}
:::

浙公网安备 33010602011771号