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\) 表示代价。接下来考虑如下流程。

  1. 弹操作序列栈。假设栈顶的分界线为 \(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\}\)
  2. 弹完栈以后,如果栈顶满足 \(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\)
  3. 如果步骤 \(2\) 的判断不成立,即做完步骤 \(1\) 后栈为空或者 \(nd.pos<pos\),那么还有一段需要修改:
    • \(cost:=\min\{cost+2\times(optpos-nd.pos),\max\{0,2\times(m-nd.pos)-1\}\}\)\(optpos:=pos\)
  4. 做完上面的所有,新加入代表这个操作的分界线,\(\{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;
}

:::

posted @ 2026-08-17 16:29  fydj  阅读(9)  评论(0)    收藏  举报