题解 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)\) 时间复杂度之内查询区间最值。这可以扩展运用,而且跟前缀和很像。在笔者看来,这更像一颗线段树。反正都是一种空间换时间的策略。
总结归纳
- 积累二分图建模的经验;
- 培养严谨的贪心策略分析手段;
- 掌握空间换时间的经典策略。

浙公网安备 33010602011771号