双栈排序

https://blog.csdn.net/linwh8/article/details/52606751
题目如下:
sicily 双栈排序
Description
Tom最近在研究一个有趣的排序问题。如图所示,通过2个栈S1和S2,Tom希望借助以下4种操作实现将输入序列升序排序。

操作a:如果输入序列不为空,将第一个元素压入栈S1
操作b:如果栈S1不为空,将S1栈顶元素弹出至输出序列
操作c:如果输入序列不为空,将第一个元素压入栈S2
操作d:如果栈S2不为空,将S2栈顶元素弹出至输出序列

如果一个1~n的排列P可以通过一系列操作使得输出序列为1,2,…,(n-1),n,Tom就称P是一个“可双栈排序排列”。例如(1,3,2,4)就是一个“可双栈排序序列”,而(2,3,4,1)不是。
下图描述了一个将(1,3,2,4)排序的操作序列:<a,c,c,b,a,d,d,b>
当然,这样的操作序列有可能有几个,对于上例(1,3,2,4),<a,c,c,b,a,d,d,b>是另外一个可行的操作序列。Tom希望知道其中字典序最小的操作序列是什么。

Input
输入有多组Case,每个Case第一行是一个整数n(n<=1000)。
第二行有n个用空格隔开的正整数,构成一个1~n的排列。
Output
每组Case输出一行,如果输入的排列不是“可双栈排序排列”,输出数字0;否则输出字典序最小的操作序列,每两个操作之间用空格隔开,行尾没有空格。
Sample Input
4
1 3 2 4
4
2 3 4 1
Sample Output
a b a a b b a b
0

注:此题有多种解题版本,但网上代码质量参差不齐,有待检验,对于初学者很难分辨正误,容易误入歧途,况且有些方法较为复杂,没有必要学习。因此,本文以一个初学者角度,对双栈排序进行详细解析。在此声明:本文代码已通过sicily评测, 文章内容相关引用已用带有底色方框标记。

1.Introduction
双栈排序,顾名思义,就是利用两个栈,对一串数字进行排序。但是,这种排序方式与其他排序方式有一定的差别,它并不能满足所有数串的排序。因此,对于能进行双栈排序的数串,我们称之为“可双栈排序序列”,反之,称之为“不可双栈排序序列”。

读者可以先根据“a b a a b b a b”自行尝试一下1 3 2 4的双栈排序,了解它的大致过程。

对于本题,我采用的方法是二分图染色法。那么什么是二分图?什么是染色法?怎么构造二分图?怎么染色?这是完成这道题之前必须回答的问题,同时也是网上教程欠缺的重要内容。

2.Bipartite graph(二分图)
二分图又称作二部图,是图论中的一种特殊模型。 设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

对于学过离散数学的同学,就很容易理解上面这段定义了。
G=(V, E),其中G代表图,V代表这个图的所有顶点的集合,E代表这个图所有连线的集合,即是边集。现如今如果能将V这个顶点集分成两个互不相交的子集A、B,E这个边集内所有边的两个顶点分别属于A、B两个子集的话,我们就称这个图为二分图

没有学过的同学看一下下面的这张图,再参照上面的定义,应该也能了解得差不多吧。

了解了二分图是什么东西后,接下来我们需要了解染色这一概念。

什么是染色?就是给对象标记一种“颜色”(这里的颜色其实可以用数字定义),其主要作用是便于分类、记,而不是实现真正意义上的染色。
故我们可以创建一个数组,每个数组的下标对应一个元素,数组的每个元素的值代表该对象标记的颜色。如此,就完成了染色这一步骤。

而且,染色还可以用于判别一个图是否是二分图。下文会提到。

在数据结构中,我们可以通过建立二维数组(布尔矩阵) picture[MAX][MAX]来实现二分图。二维数组的横向index代表A顶点子集,纵向index代表B顶点子集(当然也可以倒过来)。如若两个顶点i, j有连接,则在对应的picture[i][j] 与 picture[j][i]处赋值为1或true。

虽然二维数组可以代表图,但并不代表每一个二维数组都是二分图。那么如何检验呢?那就要通过深度优先搜索dfs进行染色验证了。

对于深度优先搜索,我只提供相关链接,就不详细阐述了:Depth-first_search
如果上面链接看完后还是不懂,可以自行查书或者百度相关博客进行学习。

二分图的条件:通过深度优先搜索对每个顶点进行染色,如果相邻顶点颜色相同或者是同一顶点出现染不同颜色的情况,则说明该图不是二分图。

3.Solution
(注:引用部分来自博文sicily双栈排序)

双栈排序的方法便是二分图染色法。即对数字串进行两个两个逐次遍历,如果符合规则A的便将二者连接起来,则当遍历结束后完成构图。然后通过dfs染色,确立真正的二分图。然后借助染色表(记录每个元素颜色的数组),进行相应的push(), pop()操作,最终便能完成排序。

而这一规则A便是:

考虑对于任意两个数q[i]和q[j],它们不能压入同一个栈中的充要条件: 存在一个k,使得i<j<k且q[k]<q[i]<q[j]。

其证明过程如下:

定理:考虑对于任意两个数q[i]和q[j],它们不能压入同一个栈中的充要条件: 存在一个k,使得i<j<k且q[k]<q[i]<q[j]。

证明:
充分性:即如果满足上述条件,那么q[i]和q[j]一定不能压入同一个栈。

证明(反证法)假设这两个数压入了同一个栈,那么压入q[k],因为q[k]比q[i]和q[j]都小,所以很显然,当q[k]没有被弹出的时候,另两个数也都不能被弹出(否则输出序列的数字顺序就不是1,2,3,…,n了)。而之后,无论其它的数字在什么时候被弹出,q[j]总是会在q[i]之前弹出,而q[j]>q[i],这显然是不正确的.

必要性:如果两个数不可以压入同一个栈,那么它们一定满足上述条件。

证明逆否命题:也就是”如果不满足上述条件,那么这两个数一定可以压入同一个栈。”不满足上述条件有两种情况:
情况1:对于任意i<j<k且q[i]<q[j],q[k]>q[i];(即对任意三个数,最小的总是在最前面)
情况2:对于任意i<j,q[i]>q[j]。
第一种情况:在q[k]被压入栈的时候,q[i]已经被弹出栈。那么,q[k]不会对q[j]产生任何影响(这里可能有点乱,因为看起来,q[j]<q[k]的时候是会有影响的,但实际上,这还需要另一个数r,满足j<k<r且 q[r]<q[j]<q[k],也就是证明充分性的时候所说的情况。而事实上我们现在并不考虑这个r,所以说q[k]对q[j]没有影响)。
第二种情况:可以发现这其实就是一个降序序列,所以所有数字都可以压入同一个栈。这样,原命题的逆否命题得证,所以原命题得证。

解题步骤

(1) 检查数列中的数字是否满足进入同一个栈的条件,如果不满足则将边连上,构造二分图。
(2) 用dfs染色,把二分图染成1和2两种颜色,使得染色为1的结点被压入s1栈,染色为2结点被压入s2栈。具体:每次选取一个未染色的编号最小的结点,将它染色为1,并从它开始dfs染色,直到所有结点都被染色为止。这样,我们就得到了每个结点应该压入哪个栈中。注意dfs结束之后未被染色的数字是可以被放入同一个栈的,所以优先染色为1。如果发现某一个数字两次要染不同的颜色,则为不能输出。

我个人的代码:

include

include

// The MAX is suitable for 1001(at least), but I set it to 1003 for safe.

define MAX 1003

using namespace std;

bool picture[MAX][MAX]; // bipartite graph
int color[MAX]; // The color of elements
int temp[MAX]; // The storage of elements that we want to sort
int small[MAX]; // An array use for judge whether the two numbers obey the rule
int num; // The number of elements we want to enter
bool flag; // Make up whether the string of number can fit into two-stack sort

/* set_color: use dfs to set the color /
void set_color(int i, int c) {
color[i] = c;
for (int j = 0; j < num; j++) {
if (picture[i][j]) { // Find the adjacent point
if (color[j] == c) { // adjacent points have the same color it will be wrong
flag = false;
}
if (!color[j]) { // if the point has never be drawn, set the color of it
set_color(j, 3-c); // 3-c only have two result: 1, 2, represent stack1 and stack2
}
}
}
}
/
Creat_Picture: create the bipartite graph with regard to the rule */
void Creat_Picture() {
small[num] = 0x7FFFFFFF; // INT_MAX, you can set the number that bigger than the elements you put
for (int i = num-1; i >= 0; i–) {
small[i] = temp[i];
if (small[i+1] < small[i]) { // create the array for the rule
small[i] = small[i+1];
}
}
for (int i = 0; i < num-1; i++) {
for (int j = i+1; j < num; j++) {
if (temp[i] < temp[j] && small[j+1] < temp[i]) {
picture[i][j] = picture[j][i] = 1; // create the gragh according to the rule
}
}
}
for (int i = 0; i < num; i++) {
if (!color[i]) { // set the default color to 1 for the unsigned one
set_color(i, 1);
}
}
}

void Calculate() {
if (flag == false) {
cout << “0” << endl;
return;
}
stack stack1; // store the element that has color one
stack stack2; // store the element that has color two
int count = 0; // Use for judging whether we should add the space
int aim = 1; // Use for judging whether we should pop the element
for (int i = 0; i < num; i++) {
if (color[i] == 1) {
stack1.push(temp[i]);
count++;
cout << “a”;
if (count < num2) {
cout << " ";
}
} else {
stack2.push(temp[i]);
count++;
cout << “c”;
if (count < num
2) {
cout << " ";
}
}
// while-loop is important, we should pop the element after we push the element
while ((!stack1.empty() && stack1.top() == aim) ||
(!stack2.empty() && stack2.top() == aim)) {
if (!stack1.empty() && stack1.top() == aim) {
stack1.pop();
count++;
aim++;
cout << “b”;
if (count < num2) {
cout << " ";
}
} else {
stack2.pop();
count++;
aim++;
cout << “d”;
if (count < num
2) {
cout << " ";
}
}
}
}
cout << endl;
}

int main(void) {
while(cin >> num) {
flag = true;
for (int i = 0; i < MAX; i++) {
color[i] = 0;
temp[i] = 0;
small[i] = 0;
for (int j = 0; j < MAX; j++) {
picture[i][j] = 0;
}
}
for (int i = 0; i < num; i++) {
cin >> temp[i];
}
Creat_Picture();
Calculate();
}
return 0;
}

大家可以结合我的博客内容和代码了解双栈排序。可能单看内容比较难理解,所以推荐结合起来看。由于我代码中有较为详细的注释,这里就不多说了。

下面看到代码中while循环:

    while ((!stack1.empty() && stack1.top() == aim) ||
           (!stack2.empty() && stack2.top() == aim)) {
        if (!stack1.empty() && stack1.top() == aim) {
            stack1.pop();
            count++;
            aim++;
            cout << "b";
            if (count < num*2) {
                cout << " ";
            }
        } else {
            stack2.pop();
            count++;
            aim++;
            cout << "d";
            if (count < num*2) {
                cout << " ";
            }
        }
    }

本题有一种错误的思路便是每次压栈前将栈顶元素与要压入的元素比较,如若栈顶大,则正常插入;如果栈顶小,则先弹出栈顶,直至栈顶大于压入元素或者栈顶为空时再压入。这种思路会导致得不到字典序最小的字符串。假如我栈1可以弹出元素,而你下一步是压栈2,那么你的操作可能是ca,但显然此处ac也是合理的,且字典序更小。所以正确的思路便是在每一次压栈后进行弹出操作。

当时我回看这段代码时,总感觉有点不对劲。因为题目要求输出的是字典序最小的操作。假如有这种情况:我下一个元素压栈1,但在这之前根据while循环是将栈1栈2所有能弹出的部分都弹出。如果我先弹出栈1中能弹出的内容,然后在压栈1,再弹出栈2中可以弹出的内容,那么岂不是能构成字典序更小的字符串?但我多方尝试,也没有找到能符合这种情况的数据。也许该算法避免了这种情况的发生,即这种情况在这个算法下是不可能事件。所以我觉得可能是我多虑了。

4.Conclusion
这道双栈排序涉及到了二分图的构造与检验,染色法,深度优先搜索,数据结构等知识,含金量非常高,自己感觉收获颇丰,有兴趣的同学推荐学习!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值