[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 栈顶元素弹出至输出序列。
Tom 希望知道其中字典序最小的操作序列是什么,若无合法方案则输出 0。
题目分析
Part 1. 如何判断是否有解
如果这道题目只有一个栈,那么就会有一些序列是无法仅通过一个栈就实现排序的,然而题目给了我们两个栈,手动模拟就会发现第二个栈起到了缓存的作用。接下来分析什么时候第二个栈可以起到缓存的作用。
设序列 A = { 3 , 2 , 1 } A=\{3,2,1\} A={3,2,1}, S 1 = S 2 = { } S_1=S_2=\{\} S1=S2={},按下标 i i i 从 1 1 1 到 n n n 遍历 A A A,则有:
- 当 i = 1 i=1 i=1 时,此时把 A 1 A_1 A1 放入 S 1 S_1 S1 和 S 2 S_2 S2 都是相同的,而考虑到字典序最小,先将 A 1 = 3 A_1=3 A1=3 放入 S 1 S_1 S1,则 S 1 = { 3 } , S 2 = { } S_1=\{3\},S_2=\{\} S1={3},S2={}。
- 当 i = 2 i=2 i=2 时,有两种可能性:
(1). S 1 = { 3 , 2 } , S 2 = { } S_1=\{3,2\},S_2=\{\} S1={3,2},S2={}
(2). S 1 = { 3 } , S 2 = { 2 } S_1=\{3\},S_2=\{2\} S1={3},S2={2}- 无论把 A 3 = 1 A_3=1 A3=1 放入 S 1 S_1 S1 或 S 2 S_2 S2,都可以按顺序弹出,可以证明都可以构造合法方案。
实际上,由于结论只和三个数之间的相对大小关系有关,我们可以将 3 , 2 , 1 3,2,1 3,2,1 替换为任意的 x , y , z ( x > y > z ) x,y,z(x>y>z) x,y,z(x>y>z)。
由此得出结论,对于任意
i
<
j
<
k
i<j<k
i<j<k,若
A
i
>
A
j
>
A
k
A_i>A_j>A_k
Ai>Aj>Ak,三者放入两个栈中的哪一个栈是不受任何限制的。
受此启发,我们只需将
A
i
,
A
j
,
A
k
A_i,A_j,A_k
Ai,Aj,Ak 三者的大小关系分类讨论并找出限制条件即可。分类讨论的标准是三个数的大小关系,共六种情况。
首先解决 A i A_i Ai 为最大值的情况。因为 i < j < k i<j<k i<j<k,所以它是第一个进栈的。当 A i A_i Ai 为最大值时,它在三个数中需要最后一个出栈,也就是说当它出栈时别的数都需要已经进入且弹出栈。也就是说,操作序列形如
push(Ai),xx,xx,xx,xx,pop(Ai)的样子。而 A j A_j Aj 和 A k A_k Ak 两个数用两个栈进行排序是完全没有问题的,所以 A i A_i Ai 为最大值的两种情况是没有任何限制的。
其次, A i A_i Ai 为最小值时, A i A_i Ai 需要第一个进栈且第一个出栈,那么只需 A i A_i Ai 进栈之后立马出栈,问题就变成了两个排序,也是可以解决的。
同理, A j A_j Aj 为最小值时, A j A_j Aj 需要第一个出栈,稍微枚举一下情况就可以验证, A j A_j Aj 只要进栈后即出栈,无论 A i , A k A_i,A_k Ai,Ak 在哪个栈中都可以解决问题。
所得结论如下:
| A i , A j , A k A_i,A_j,A_k Ai,Aj,Ak 三数大小关系 | 放入栈 S 1 S_1 S1 或 S 2 S_2 S2 的限制 |
|---|---|
| A i > A j > A k A_i>A_j>A_k Ai>Aj>Ak | 无限制 |
| A i > A k > A j A_i>A_k>A_j Ai>Ak>Aj | 无限制 |
| A j > A i > A k A_j>A_i>A_k Aj>Ai>Ak | - |
| A j > A k > A i A_j>A_k>A_i Aj>Ak>Ai | 无限制 |
| A k > A i > A j A_k>A_i>A_j Ak>Ai>Aj | 无限制 |
| A k > A j > A i A_k>A_j>A_i Ak>Aj>Ai | 无限制 |
接下来解决最后一个
A
k
<
A
i
<
A
j
A_k<A_i<A_j
Ak<Ai<Aj 的问题。
A
k
A_k
Ak 是最小值,所以
A
k
A_k
Ak 必定最后一个进栈且第一个出栈。由于
A
k
A_k
Ak 是第一个出栈的,所以其他出栈操作均需在
A
k
A_k
Ak 出栈后进行,其他进栈操作均需在
A
k
A_k
Ak 进栈前进行,操作序列就变为了 push(),push(),push(Ak),pop(Ak),pop(),pop()。
结合已知的条件,序列进一步确定为 push(Ai),push(Aj),push(Ak),pop(Ak),pop(Ai),pop(Aj)。注意到
A
i
A_i
Ai 比
A
j
A_j
Aj 先进栈却后出栈,不符合栈的规律,所以
A
i
A_i
Ai 和
A
j
A_j
Aj 不能处于同一个栈中。
| A i , A j , A k A_i,A_j,A_k Ai,Aj,Ak 三数大小关系 | 放入栈 S 1 S_1 S1 或 S 2 S_2 S2 的限制 |
|---|---|
| A i > A j > A k A_i>A_j>A_k Ai>Aj>Ak | 无限制 |
| A i > A k > A j A_i>A_k>A_j Ai>Ak>Aj | 无限制 |
| A j > A i > A k A_j>A_i>A_k Aj>Ai>Ak | A i A_i Ai 与 A j A_j Aj 不能处于同一个栈中 |
| A j > A k > A i A_j>A_k>A_i Aj>Ak>Ai | 无限制 |
| A k > A i > A j A_k>A_i>A_j Ak>Ai>Aj | 无限制 |
| A k > A j > A i A_k>A_j>A_i Ak>Aj>Ai | 无限制 |
现在我们就发现了这道题最重要的性质:
对于任意的 i < j < k i<j<k i<j<k,若 A k < A i < A j A_k<A_i<A_j Ak<Ai<Aj,则 A i , A j A_i,A_j Ai,Aj 不能同时处于一个栈中。
由此可知,我们可以根据数的大小关系得出某些数是不能同时处于一个栈中的,所以把不能处于栈中的点对
(
A
i
,
A
j
)
(A_i,A_j)
(Ai,Aj) 连边,在产生的图中进行二分图判定即可。如果所得的图不是二分图,就说明原序列不能用双栈排序解决。(经本人测试,完成本部分代码可以获得 10pts 的高分)
Part 2. 如何获取字典序最小的方案
接下来考虑如何求得方案。判定是否为二分图时,dfs 的过程中就已经得出了每个元素应被划分为二分图的哪一个部分。令与
A
1
A_1
A1 处于同一个部分的元素都进入
S
1
S_1
S1,与
A
1
A_1
A1 处于不同部分的元素进入
S
2
S_2
S2 就可以构造出方案,但是字典序并非最小。(经本人测试,直接输出方案就可以通过全部官方数据,但各大 OJ 基本已设置了 hack 数据)
然而,题目要求获取字典序最小的方案。于是考虑如何让当前的答案的字典序变得更小。每一个 a 和 b、c 和 d 的相对顺序无法改变,a 和 b、c 和 d 的相对顺序也无法改变,因此只能改变
(
a
,
d
)
(a,d)
(a,d) 或
(
b
,
c
)
(b,c)
(b,c) 的相对顺序。
那就规定 a 和 d 属于同一种字符,b 和 c 属于同一种字符,将已经求得的答案分为不同的段,每段之内排序即可。
例如 acabbabdab 分段为 a/c/a/bb/a/da/b,段内排序后变为 a/c/a/bb/a/ad/b,也就是 acabbaadb,获得了一个字典序更小的方案。 虽暂时无法证明这是最优的,但是足以通过 OJ 的测试数据。(如果有证法欢迎补充)
简要题解
手动模拟可以发现如下性质:
对于任意的 i < j < k i<j<k i<j<k,若 A k < A i < A j A_k<A_i<A_j Ak<Ai<Aj,则 A i , A j A_i,A_j Ai,Aj 不能同时处于一个栈中。
把不能处于同一个栈中的点对
(
A
i
,
A
j
)
(A_i,A_j)
(Ai,Aj) 连边,在产生的图中进行二分图判定即可。如果所得的图不是二分图,就说明原序列不能用双栈排序解决。(经本人测试,完成本部分代码可以获得 10pts 的高分)
接下来考虑如何求得方案。判定是否为二分图时,dfs 的过程中就已经得出了每个元素应被划分为二分图的哪一个部分。按照每个点所处的部分对应元素应进入哪一个栈,可以构造出字典序并非最小的方案。(经本人测试,直接输出方案就可以通过全部官方数据,但各大 OJ 基本已设置了 hack 数据)
接下来考虑优化已经求得的压栈/弹栈方案,使其字典序更小。由于只有
(
a
,
d
)
(a,d)
(a,d) 或
(
b
,
c
)
(b,c)
(b,c) 的相对顺序可以改变,就规定 a 和 d 属于同一种字符,b 和 c 属于同一种字符,将已经求得的答案分为不同的段,每段之内排序即可。
例如 acabbabdab 分段为 a/c/a/bb/a/da/b,段内排序后变为 a/c/a/bb/a/ad/b,也就是 acabbaadb,获得了一个字典序更小的方案。
虽暂时无法证明这是最优的,但是足以通过 OJ 的测试数据。(如果有证法欢迎补充)
参考代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
inline int read(){
int s = 0, w = 1;
char ch = getchar();
for(; ch < '0' || ch > '9'; w *= ch == '-' ? -1 : 1, ch = getchar());
for(; ch >= '0' && ch <= '9'; s = 10 * s + ch - '0', ch = getchar());
return s * w;
}
const int MAXN = 2005;
const int MAXM = 500005;
struct Graph{
struct Edge{
int to, nxt;
} e[MAXM << 1];
int head[MAXN], tot;
void add(int u, int v){
e[++tot].to = v;
e[tot].nxt = head[u];
head[u] = tot;
}
} G;
int N, a[MAXN], sufmin[MAXN], co[MAXN], op[MAXN << 1], res[MAXN], Index;
bool judge(int u, int fa, int color){
co[u] = color;
for(int i = G.head[u], v; i; i = G.e[i].nxt){
v = G.e[i].to;
if(v == fa) continue;
if(co[v] == co[u]) return false;
if(judge(v, u, -color) == false) return false;
}
return true;
}
void work(int l, int r){
int cnt1 = 0, cnt2 = 0, bas = min(res[l], 5 - res[l]);
for(int i = l; i <= r; i++){
if(res[i] * 2 < 5) cnt1++;
else cnt2++;
}
for(int i = l; i <= r; i++){
if(i <= l + cnt1 - 1) res[i] = bas;
else res[i] = 5 - bas;
}
}
signed main(){
N = read();
for(int i = 1; i <= N; i++) a[i] = read();
sufmin[N] = a[N];
for(int i = N - 1; i >= 1; i--) sufmin[i] = min(sufmin[i + 1], a[i]);
for(int i = 1; i <= N; i++){
for(int j = i + 1; j <= N; j++){
if(sufmin[j] < a[i] && a[i] < a[j]){
G.add(i, j), G.add(j, i);
}
}
}
for(int i = 1; i <= N; i++){
if(co[i]) continue;
if(judge(i, i, 1) == false){
cout << 0 << endl;
return 0;
}
}
stack<int> st1, st2;
int cur = 1;
for(int i = 1; i <= N; i++){
if(co[i] > 0) st1.push(a[i]), res[++Index] = 1;
else st2.push(a[i]), res[++Index] = 3;
while((!st1.empty() && st1.top() == cur) || (!st2.empty() && st2.top() == cur)) {
if(!st1.empty() && st1.top() == cur){
st1.pop(), res[++Index] = 2;
} else {
st2.pop(), res[++Index] = 4;
}
cur++;
}
}
while((!st1.empty() && st1.top() == cur) || (!st2.empty() && st2.top() == cur)) {
if(!st1.empty() && st1.top() == cur){
st1.pop(), res[++Index] = 2;
} else {
st2.pop(), res[++Index] = 4;
}
cur++;
}
int lst = -1, lstpos = 0;
for(int i = 1; i <= Index; i++){
if(min(res[i], 5 - res[i]) != lst){
work(lstpos, i - 1);
lstpos = i;
}
lst = min(res[i], 5 - res[i]);
}
for(int i = 1; i <= Index; i++){
cout << (char)(res[i] - 1 + 'a') << " ";
}
cout << endl;
return 0;
}
505

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



