[NOIP2008 提高组] 双栈排序 题解

[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,则有:

  1. 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={}
  2. 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}
  3. 无论把 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 数据)

然而,题目要求获取字典序最小的方案。于是考虑如何让当前的答案的字典序变得更小。每一个 abcd 的相对顺序无法改变,abcd 的相对顺序也无法改变,因此只能改变 ( a , d ) (a,d) (a,d) ( b , c ) (b,c) (b,c) 的相对顺序。

那就规定 ad 属于同一种字符,bc 属于同一种字符,将已经求得的答案分为不同的段,每段之内排序即可。

例如 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) 的相对顺序可以改变,就规定 ad 属于同一种字符,bc 属于同一种字符,将已经求得的答案分为不同的段,每段之内排序即可。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值
>