题解 luogu.P1155 [NOIP 2008 提高组] 双栈排序

题目

luogu.P1155 [NOIP 2008 提高组] 双栈排序

这个题目还是很有难度的。难在两点:
  • 建模极其困难;
  • 贪心极其诡异。
    我们分别来看。

题意建模

‌第一步:理解题意‌

核心需求‌:

给定\(1...n\)的排列,通过两个栈\(S1\)\(S2\)和四种操作(a:压入\(S1\),b:弹出\(S1\),c:压入\(S2\),d:弹出\(S2\)),能否输出升序序列?若可行,输出字典序最小的操作序列‌。

‌第二步:建模分析(四步法)‌

‌1.两类对象‌

左部集:

序列中的元素(值)

右部集:

两个栈(抽象为两种颜色)

‌2.冲突边建立‌

当存在三元组 \((i,j,k)\) 满足 \(i<j<k\)\(a[k]<a[i]<a[j]\) 时,\(i\)\(j\) 不能在同一栈(需染色不同)‌

3.建图:

冲突的元素间连无向边

‌4.目标验证‌

  • 二分图染色判定可行性
  • 染色结果决定元素归属栈(颜色 \(1→S1\),颜色 \(2→S2\))‌

‌5.操作生成‌

  • 按字典序优先选择操作 a(压入\(S1\)
  • 模拟时根据染色结果选择压栈顺序‌

算法分析

还是按照我们用的“四部曲”来分析。这道题左右部集的选取不算难,但是难度大的是贪心。
肯定是存在依赖的,也就是元素到栈的映射。所以这样就确立了两个集合。

另一方面,
当存在三元组 \((i,j,k)\) 满足 \(i<j<k\)\(a[k]<a[i]<a[j]\)时,元素\(i\)\(j\)必须分配到不同栈。这是因为:

\(i\)\(j\)同栈,由于\(i\)先入栈,\(j\)后入栈,但\(a[i]<a[j]\)导致\(j\)必须先于\(i\)弹出(否则无法形成升序),这与栈的LIFO特性矛盾‌;
该条件通过min_Af[j+1]<a[i]快速检测,其中min_suf[j+1]表示\(j+1\)位置之后的最小值,确保存在\(k>j\)使\(a[k]<a[i]‌\)。代码实现如下:

参考代码

#include<iostream>
#include<cstdio>
#include<stack>
#include<cstring>
using namespace std;
const int N=2e3+5;
struct edge{int next,to;}e[N];
int head[N],idx;
int n,ans,col[N],a[N],min_A[N];
stack<int> s1,s2;
int cnt=1;
inline void add(int u,int v){e[++idx]={head[u],v};head[u]=idx;}
bool dfs(int u,int c)
{
	col[u]=c;
	for(int i=head[u];i;i=e[i].next)
	{
		int v=e[i].to;
		if(col[v]==c) return false;
		if(col[v]==-1 && !dfs(v,c^1)) return false;
 	}
 	return true;
}
void popall(int lim)
{
	while(1)
	{
		if(s1.size() && s1.top()==cnt)
		{
			cnt++,printf("%c ",'b');
			s1.pop();
		} 
		else if(s2.size() && s2.top()==cnt) 
		{
			if(cnt==lim-1) break;
			cnt++;
			printf("%c ",'d');s2.pop();
		}
		else break;
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++) scanf("%d",&a[i]);
	min_A[n+1]=n+1;
	for(int i=n;i>=1;i--) min_A[i]=min(min_A[i+1],a[i]);
	
	for(int i=1;i<=n;i++)
		for(int j=i+1;j<n;j++)
			if(a[i]<a[j]&&min_A[j+1]<a[i]) 
				add(i,j),add(j,i);
	
	memset(col,-1,sizeof(col));
	for(int i=1;i<=n;i++)
		if(col[i]==-1 && !dfs(i,0))
			{ puts("0"); return 0; }
	
	for(int i=1;i<=n;i++)
	if(col[i]==0)
	{
		bool f=false;
		while(s1.size() && s1.top()==cnt) 
		{
			s1.pop(),printf("%c ",'b');
			cnt++; f=true;		
		}
		if(s1.size() && s1.top()<a[i]) popall(a[i]);
		s1.push(a[i]);
		printf("%c ",'a');
	}	
	else popall(0),s2.push(a[i]), printf("%c ",'c');
	popall(0);
	return 0;		
}

细节实现

贪心策略正确性的考究

会存在没有弹干净的影响,所以考虑构造一个函数,我们贪心的策略是:优先考虑 \(S1\),能进就先进,能出就先出。比这还没有研究透彻,所以这一部分的内容要稍等。

前缀和思想的扩展应用

本来是 \(O(n^{3})\) 的时间复杂度,但是由于有着 min_A[]数组的作用,我们可以在 \(O(1)\) 时间复杂度之内查询区间最值。这可以扩展运用,而且跟前缀和很像。在笔者看来,这更像一颗线段树。反正都是一种空间换时间的策略。

总结归纳

  1. 积累二分图建模的经验;
  2. 培养严谨的贪心策略分析手段;
  3. 掌握空间换时间的经典策略。
posted @ 2025-07-25 21:34  枯骨崖烟  阅读(9)  评论(0)    收藏  举报