【二分图+栈排序】题解:P1155 [NOIP2008 提高组] 双栈排序_二分图染色_贪心_模拟_C++算法竞赛

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 1n 的排列 P P P 可以通过一系列合法操作使得输出序列为 ( 1 , 2 , ⋯   , n − 1 , n ) (1,2,\cdots,n-1,n) (1,2,,n1,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 1n 的排列。

输出格式

共一行,如果输入的排列不是“可双栈排序排列”,输出 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 n10

50 % 50\% 50% 的数据满足: n ≤ 50 n\le50 n50

100 % 100\% 100% 的数据满足: n ≤ 1000 n\le1000 n1000

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值