P1155 [NOIP2008 提高组] 双栈排序
题目描述
Tom 最近在研究一个有趣的排序问题。如图所示,通过
2
2
2 个栈
S
1
S_1
S1 和
S
2
S_2
S2,Tom 希望借助以下
4
4
4 种操作实现将输入序列升序排序。

- 操作 a \verb!a! a:将第一个元素压入栈 S 1 S_1 S1。
- 操作 b \verb!b! b:将 S 1 S_1 S1 栈顶元素弹出至输出序列。
- 操作 c \verb!c! c:将第一个元素压入栈 S 2 S_2 S2。
- 操作 d \verb!d! d:将 S 2 S_2 S2 栈顶元素弹出至输出序列。
如果一个 1 ∼ n 1\sim n 1∼n 的排列 P P P 可以通过一系列合法操作使得输出序列为 ( 1 , 2 , ⋯ , n − 1 , n ) (1,2,\cdots,n-1,n) (1,2,⋯,n−1,n),Tom 就称 P P P 是一个“可双栈排序排列”。例如 ( 1 , 3 , 2 , 4 ) (1,3,2,4) (1,3,2,4) 就是一个“可双栈排序序列”,而 ( 2 , 3 , 4 , 1 ) (2,3,4,1) (2,3,4,1) 不是。下图描述了一个将 ( 1 , 3 , 2 , 4 ) (1,3,2,4) (1,3,2,4) 排序的操作序列: a,c,c,b,a,d,d,b \texttt {a,c,c,b,a,d,d,b} a,c,c,b,a,d,d,b。

当然,这样的操作序列有可能有几个,对于上例 ( 1 , 3 , 2 , 4 ) (1,3,2,4) (1,3,2,4), a,b,a,a,b,b,a,b \texttt{a,b,a,a,b,b,a,b} a,b,a,a,b,b,a,b 是另外一个可行的操作序列。Tom 希望知道其中字典序最小的操作序列是什么。
输入格式
第一行是一个整数 n n n。
第二行有 n n n 个用空格隔开的正整数,构成一个 1 ∼ n 1\sim n 1∼n 的排列。
输出格式
共一行,如果输入的排列不是“可双栈排序排列”,输出 0。
否则输出字典序最小的操作序列,每两个操作之间用空格隔开,行尾没有空格。
样例 #1
样例输入 #1
4
1 3 2 4
样例输出 #1
a b a a b b a b
样例 #2
样例输入 #2
4
2 3 4 1
样例输出 #2
0
样例 #3
样例输入 #3
3
2 3 1
样例输出 #3
a c a b b d
提示
30 % 30\% 30% 的数据满足: n ≤ 10 n\le10 n≤10。
50 % 50\% 50% 的数据满足: n ≤ 50 n\le50 n≤50。
100 % 100\% 100% 的数据满足: n ≤ 1000 n\le1000 n≤1000。
2021.06.17 加强 by SSerxhs。hack 数据单独分为一个 subtask 防止混淆。
noip2008 提高第四题
题解
/*
这道题有难度。遇到这种题我们先找规律:假设只有一个栈,那满足什么条件的序列无法成功排序呢?
尝试一下:1,2,3的所有排列中,只有2,3,1不行。1,2,3,4的所有排列中,1,3,4,2不行
我们只看[2,3,1]和[3,4,2],不难找到一个共性:
若a[i],a[j],a[k]满足i<j<k,且a[i]<a[j],a[i]>a[k],则无法完成单栈排序
简单证明:这样的话会使较大的a[j]压在较小的a[i]上,且a[k]无法在a[i]之前出栈。所以一定会造成矛盾的情况。
考虑如何判断数字冲突:
三层循环太费时间,可以只枚举(i,j),对于a[k]预处理一个后缀最小值Min。
若a[i]>Min[j+1],则后面一定有一个(或若干个)不满足条件的a[k]
所以,这就是需要双栈排序的原因了。
那么我们要思考一个问题:哪些数字要进入第一个栈,哪些数字要进入第二个栈
很明显就是冲突了的那些数字,只有让那些数字不进入同一个栈,才有成功排序的可能。
我们很自然地就会想到————二分图染色。对于每一对a[i],a[j],建一条无向边,然后对图进行二分图判定+染色
如果能成功染色,则说明有合法的方案,反之则不能
接下来可以通过用两个栈模拟的方式求出操作步骤。(比较难以解释,详见代码)
----------------------------------------------------------------
然后,题目的另一个难点出现了:要求操作字典序最小(压入第一个栈<弹出第一个栈<压入第二个栈<弹出第二个栈)
我们肯定要对每个处理步骤一顿贪心。
我们假设染色时,染成0要进入第一个栈,染成1要进入第二个栈
对于二分图染色判断,从小到大枚举所有的点,并且在进行染色时以0开始。这样染完色之后序号较小的一定会进入第一个栈,减小字典序
然后模拟时,对于字典序小的操作,优先做。即能先做a就先做a,再能先做b就先做b
这里还有一个坑点:
假设S1=[7,5],S2=[8,6],我们要把9放入栈S1中,
正常第一眼想到的方案是:先把所有<9的都弹栈,再把9放入S1,序列:bdbda
但这是错的,最优的方案是:当7弹栈后,就把9放入栈S1中,这样会节省字典序,序列:bdbad
也就是说,假设要把x放入栈St,那么只要满足 St为空 或 St.top()>x,此时就直接把x压入栈中,这样是最优的
模拟还有一些细节,见代码
*/
#include <bits/stdc++.h>
using namespace std;
const int maxn = 1005;
int n, a[maxn], Min[maxn]; // Min是a的后缀最小值
vector <int> G[maxn];
int col[maxn]; // 二分图染色数组
int dfs(int u, int c){
col[u] = c;
for(int v : G[u]){
if(col[v] == c) return 0;
if(col[v] == -1 && dfs(v, c^1) == 0) return 0;
}
return 1;
}
void solve()
{
cin >> n;
for(int i = 1; i <= n; i ++) cin >> a[i];
// 求后缀最小值
Min[n] = a[n]; // 初始化
for(int i = n - 1; i >= 1; i --){
Min[i] = min(Min[i + 1], a[i]);
}
// 判断是否存在三元组(a[i],a[j],a[k]),满足i<j<k,且a[i]<a[j],a[i]>a[k](a[k]可以利用后缀最小值来检查)
for(int i = 1; i <= n; i ++){
for(int j = i + 1; j <= n - 1; j ++){
if(a[i] < a[j] && a[i] > Min[j + 1]){ // 因为j<k,所以a[k]要取Min[j+1]
G[i].push_back(j); // 建边,准备跑二分图染色
G[j].push_back(i); // 如果两个点有边相连,则它们一定不能放在同一个栈中
}
}
}
memset(col, -1, sizeof col); // 初始化染色数组为-1
bool flag = true; // 能否成功二分图染色
for(int i = 1; i <= n; i ++){ // 按照下标从小到大遍历,这样序号小的一定先染上0,保证字典序最小
if(col[i] == -1 && dfs(i, 0) == 0) flag = false;
}
// 如果不是二分图的话,说明不能成功匹配,就可以直接返回了
if(!flag){
cout << "0\n"; return ;
}
// 然后,大模拟 染色结束后,染0的点一定入栈1,染1的点一定入栈2
stack <int> S[3]; // S[1],S[2]表示两个栈
int pos = 1; // pos表示当前该放哪个数
for(int i = 1; i <= n && pos <= n; i ++){ // 循环当前要把哪个数入栈
if(col[i] == 0){ // 应该放入第一个栈
while(pos <= a[i]){
// 在while里面,要注意按照字典序进行操作,即操作序号字典序小的先操作,这样保证输出的答案字典序最小
if(S[1].empty() || S[1].top() > a[i]){ // 这样就可以直接把a[i]放入栈,并退出循环
// !!!这是节省字典序的方法,一定要注意这个细节
S[1].push(a[i]);
cout << "a ";
break;
}
// 下面的操作是为了弹出pos,让a[i]早点入栈
else if(!S[1].empty() && S[1].top() == pos){ // 注意判栈空的情况
S[1].pop();
cout << "b ";
pos ++;
}
else if(!S[2].empty() && S[2].top() == pos){
S[2].pop();
cout << "d ";
pos ++;
}
}
}
else{ // 放入栈1同理
while(pos <= a[i]){
if(!S[1].empty() && S[1].top() == pos){
S[1].pop();
cout << "b ";
pos ++;
}
else if(S[2].empty() || S[2].top() > a[i]){
S[2].push(a[i]);
cout << "c ";
break;
}
else if(!S[2].empty() && S[2].top() == pos){
S[2].pop();
cout << "d ";
pos ++;
}
}
}
}
// 最后,如果栈中还有剩余的元素,就继续弹栈。
// 因为此时两个栈里已经排好序(从栈顶到栈底),所以只要按照大小关系输出即可
while(S[1].size() || S[2].size()){
if(S[2].empty()){
S[1].pop();
cout << "b ";
}
else if(S[1].empty()){
S[2].pop();
cout << "d ";
}
else if(S[1].top() < S[2].top()){
S[1].pop();
cout << "b ";
}
else if(S[1].top() > S[2].top()){
S[2].pop();
cout << "d ";
}
}
}
signed main()
{
ios :: sync_with_stdio(false), cin.tie(nullptr), cout.tie(nullptr);
solve();
return 0;
}
827

被折叠的 条评论
为什么被折叠?



