---
# System prepended metadata

title: Two Stacks Sorting 題解與想法

---

---
titile: Two Stack Sorting
---
# Two Stacks Sorting 題解與想法
---
## 題敘
來源: [cses](https://cses.fi/problemset/task/2402) 

---

>給定 $1$ ~ $n$ 的排列 $a_1$~$a_n$，每次可以做以下一個操作: 
>
>($b$初始為空，$S_1$、$S_2$為兩相異stack)
>* 將$a$陣列最前面的數字push進$S_1$，並將$a$陣列第一項刪除
>* 將$a$陣列最前面的數字push進$S_2$，並將$a$陣列第一項刪除
>* 將$S_1.top$加入$b$陣列末尾，並$S_1.pop()$
>* 將$S_2.top$加入$b$陣列末尾，並$S_2.pop()$
>
>請問是否能使 $1 \leq i \leq n$ , $b_i=i$ ?
>如果不行請輸出 "IMPOSIBBLE"
>如果可以請輸出 $1 \leq i \leq n$ , $a_i$ 被分配到哪個stack

---
## 提示

### Hint 1
要使$b$由小到大遞增，stack要維護什麼性質

### Hint 1.5
如果想的是greedy可以想想反例

### Hint 2
一個數什麼時候會離開stack

### Hint 3
兩個數在什麼條件下不能在同一個stack

### Hint 4
兩個數在什麼條件下必須在同一個stack

---
## 題解

首先我們可以觀察到stack由下到上一定是遞減的

:::spoiler proof
> 假設$c_1$在$c_2$上面
> 可知在$b$中$c_1$一定在$c_2$前面
> 由於$b$是遞增的
> 可知$c_1$<$c_2$
:::

而再觀察題目一下會發現，所有數字從放入stack到拿出stack會是一段時間區間
而時間區間為 : ($.in$為進入時間 $.out$為跳出時間 $n.in$為$n$在$a$中的位置)

$$
\begin{cases}
\large{n.out=(n-1).out}\quad\quad\quad(n.in<(n-1).out)\\
\large{n.out=n.in}\quad\quad\quad\quad\quad\quad(n.in>(n-1).out)
\end{cases}
$$

特別地 定義 $1.out$=$1.in$
:::spoiler proof
>如果$n$要pop到$b$的尾端，顯然$b$的尾端為$n-1$
>
>而如果$n.in$<$(n-1).out$
>在$(n-1)$被pop時$n$為stack中的最小值，所以可以同時pop
>
>如果$n.in$>$(n-1).out$
>n進入stack時顯然為最小值，可以直接pop
:::

經過這個變換可以將測資
```
5 2 4 1 6 3
```
轉換為![541263](https://hackmd.io/_uploads/SkxyVUwBa.png)
又我們可以觀察到問題能轉換為
找到一個分組方式:
$\forall x \in a$ , ($x\in S_1 \lor x\in S_2)$
$\forall x \in S_1$ , $\mathop{\land}\limits_{y\in S_1
}$(( $x\geq y$ ) $\lor$ ($x.in>y.in$) $\lor$ ($y.in > x.out$))
$\forall x \in S_2$ , $\mathop{\land}\limits_{y\in S_2
}$(( $x\geq y$ ) $\lor$ ($x.in>y.in$) $\lor$ ($y.in > x.out$))
白話文就是強迫分兩組且組內不相交
:::spoiler proof
>強迫分組顯然
>
>如果兩時間相交，不失一般性假設 $l.in<r.in<l.out<r.out$
>假設$l$,$r$同組
>因為$l.in<r.in$所以$l$在$r$之下
>所以$l.out$應該要大於$r.out$
>與假設不符
>所以$l,r$不同組
:::
這時我們就有$O(n^2logn)$作法:
```
由1~n枚舉i
對每個i枚舉i+1~n
如果相交就有一組兩兩不再同一組的條件
用經典的2-sat分組特例作法
就可以用disjointset解掉
```
:::spoiler code
``` cpp=
#include <bits/stdc++.h>
#define pii pair<int,int>
#define ff first
#define ss second
#define inv(x) (x<n?x+n:x-n)
using namespace std;
const int mx=2e5+5;
 
int p[mx<<1];
int n;
int c[mx];
int a[mx];
int pl[mx];
pii pr[mx];
 
void init(){
    for (int i=1;i<=2*n;i++){
        p[i]=i;
    }
}
 
int find(int x){
    for (int i=0;i<1000000;i++){}
    return x==p[x]?x:p[x]=find(p[x]);
}
 
void join(int x,int y){
    x=find(x),y=find(y);
    if (x!=y){
        p[x]=y;
    }
}
 
bool mk(){
    for (int i=1;i<=n;i++){
        if (find(p[i])==find(p[i+n])){
            return 0;
        }
    }
    int t1=find(1),t2=find(1+n);
    for (int i=1;i<=n;i++){
        int fi=find(i);
        if (find(fi)==t1){
            c[i]=1;
        }else if (t2==find(fi)){
            c[i]=2;
        }else{
            p[fi]=t1;
            p[find(inv(fi))]=t2;
            c[i]=1;
        }
    }
    return 1;
}
 
int main(){
    cin.tie(0);ios::sync_with_stdio(0);
 
    cin >> n;
    init();
    for (int i=1;i<=n;i++){
        cin >> a[i];
        pl[a[i]]=i;
    }
    pr[1]={pl[1],pl[1]};
    for (int i=2;i<=n;i++){
        if (pl[i]>pr[i-1].ss){
            pr[i]={pl[i],pl[i]};
        }else{
            pr[i]={pl[i],pr[i-1].ss};
        }
    }
    for (int i=1;i<n;i++){
        for (int j=i+1;j<=n;j++){
            if ((pr[i].ff<pr[j].ff&&pr[j].ff<pr[i].ss)||(pr[i].ff<pr[j].ss&&pr[j].ss<pr[i].ss)){
                p[find(i+n)]=find(j);
                p[find(j+n)]=find(i);
            }
        }
    }
    if (!mk()){
        cout << "IMPOSSIBLE\n";
    }else{
        for (int i=1;i<=n;i++){
            cout << c[a[i]] << " ";
        }
        cout << "\n";
    }
}
```
:::
### 最終版
但看看測資範圍 $n\leq2e5$
$O(n^2logn)$ 的作法顯然太暴力 我們要怎麼有效率的做呢
思考有什麼條件會使兩數同組，還有什麼時候一定"IMPOSSIBLE"
回到前面將圖畫出，~~瞪個一個禮拜，再被段考慘電~~
可以發現如果兩線段相交且相交區有還未被左界訪問過的點一定"IMPOSSIBLE"
如果兩線段有包含關係且重和區有位被左界拜訪的點，兩線段必須同組
所以我們可以~~通靈~~推論出以下作法:

```
維護一個存不相交線段的set
在每次枚舉時當作將線段insert進set
用lower_bwound找出最小的右界大於插入線段的左界
如果與第一個有相交 查詢相交區是否都被訪問過 (我用BIT 感覺可壓(?)
並刪除原本線段與新線段相交處
一路枚舉到set尾端
對於後面的線段查詢重和部分是否都被訪問過 如果還沒 將新線段與原線段放入同一組
刪除訪問到的線段
最後插入原線段
```

:::spoiler proof
> 如果兩線段重和且中間還有未被訪問的點
> 假設三線段分別為$l_a$ 、 $l_b$ 、 $l_c$
> 因為$l_c.in<l_a.out$所以 $l_c$ 不同組於 $l_a$
> 因為$l_c.in<l_b.out$所以 $l_c$ 不同組於 $l_b$
> 所以$l_a$同組於$l_b$
:::

:::spoiler AC code (16ms)
``` cpp=
#include <bits/stdc++.h>
#define pii pair<int,int>
#define ff first
#define ss second
#define inv(x) (x>n?x-n:x+n)
#pragma GCC optmize("O3")
using namespace std;
const int mx=2e5+5;
int p[mx<<1];
int n;
int c[mx];
int a[mx];
int pl[mx];
pii pr[mx];
void init(){
    for (int i=1;i<=2*n;i++){
        p[i]=i;
    }
}
int find(int x){
    for (int i=0;i<1000000;i++){}
    return x==p[x]?x:p[x]=find(p[x]);
}
void join(int x,int y){
    x=find(x),y=find(y);
    if (x!=y){
        p[x]=y;
    }
}
bool mk(){
    for (int i=1;i<=n;i++){
        if (find(p[i])==find(p[i+n])){
            return 0;
        }
    }
    int t1=find(1),t2=find(1+n);
    for (int i=1;i<=n;i++){
        int fi=find(i);
        if (find(fi)==t1){
            c[i]=1;
        }else if (t2==find(fi)){
            c[i]=2;
        }else{
            p[fi]=t1;
            p[find(inv(fi))]=t2;
            c[i]=1;
        }
    }
    return 1;
}
int bit[mx];
void insert(int id){
    for (int i=id;i<mx;i+=(i&-i)){
        bit[i]+=1;
    }
}
int qur(int l,int r){
    int sl=0;int sr=0;
    for (int i=l-1;i>0;i-=i&-i){
        sl+=bit[i];
    }
    for (int i=r;i>0;i-=i&-i){
        sr+=bit[i];
    }
    return sr-sl;
}
struct pi{
    int l,r;
    int id;
    void init(int a,int b,int c){
        l=a;
        r=b;
        id=c;
    }
    bool operator<(pi b){
        return r<b.r;
    }
};
bool operator<(pi a,pi b){
    return a.r<b.r;
}
set<pi> st;
void tdo(){
    for (int i=1;i<=n;i++){
        insert(pr[i].ff);
        pi pa;
        pa.init(0,pr[i].ff,0);
        auto it=st.lower_bound(pa);
        if (it==st.end()){
            pi pa;
            pa.init(pr[i].ff,pr[i].ss,i);
            st.insert(pa);
        }else{
            if ((*it).l<pr[i].ff){
                int ns=qur(pr[i].ff,pr[i].ss);
                if (ns<(*it).r-pr[i].ff+1){
                    cout << "IMPOSSIBLE\n";
                    exit(0);
                }
                join((*it).id,i+n);
                join((*it).id+n,i);
                pi pa;
                pa.init((*it).l,pr[i].ff,(*it).id);
                auto pre=it;
                it++;
                st.erase(pre);
                st.insert(pa);
            }
            for (auto j=it;j!=st.end();){
                if (qur((*j).l,(*j).r)<(*j).r-(*j).l+1){
                    join((*j).id,i);
                    join((*j).id+n,i+n);
                }
                auto pre=j;
                j++;
                st.erase(pre);
            }
            pi pa;
            pa.init(pr[i].ff,pr[i].ss,i);
            st.insert(pa);
        }
    }
}
 
int main(){
    cin.tie(0);ios::sync_with_stdio(0);
    cin >> n;
    init();
    for (int i=1;i<=n;i++){
        cin >> a[i];
        pl[a[i]]=i;
    }
    pr[1]={pl[1],pl[1]};
    for (int i=2;i<=n;i++){
        if (pl[i]>pr[i-1].ss){
            pr[i]={pl[i],pl[i]};
        }else{
            pr[i]={pl[i],pr[i-1].ss};
        }
    }
    tdo();
    if (!mk()){
        cout << "IMPOSSIBLE\n";
    }else{
        for (int i=1;i<=n;i++){
            cout << c[a[i]] << " ";
        }
        cout << "\n";
    }
    
}
```
:::
### 複雜度分析:
>每個線段最多被刪一次，且刪除所產生的線段最多$n$個
>每次刪除會做一次lower_bound 花 $(log n)$ $\times$ $n$
>每次被刪會做一次find和join 花 $(log n)$ $\times$ $n$
>所以時間複雜度為$O(nlogn)$
----
## 致謝
我要感謝那些我參考過~~但看不懂~~的題目想法
>[codeforce ashwanth106121023](https://codeforces.com/blog/entry/122535)
[JOISC - Port Facility: Bicoloring & Segment Trees](https://mamnoonsiam.github.io/posts/joisc-2017-port-facility)

還有niter學長和我一起討論
還有 [cses 題源](https://cses.fi/problemset/task/2402/)

---
## 後語
欸我好像忘記說右界會單調遞增了 ~~阿隨便拉顯而易見~~ <- 可以看看定義式就可以推出來
還有人家都寫線段樹我不會qwq