ABC293の解法と要点、感想(AからD問題まで)

## はじめに
A,B,Cの3完で39:12+1ペナでした。
コンテスト後にDも解いたので、それについてもまとめておきます。
[コンテスト成績証](https://atcoder.jp/users/sorachandu/history/share/abc293)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2902990/0da8cdce-420c-5dd6-e79a-0b34429d44c3.png)
なお、記事内で登場するコードには独自のテンプレートやマクロが含まれている場合があります。
それらの詳細については、お手数ですが[こちらの記事](https://qiita.com/sorachandu/items/041169d34b9f9b99bcf7#%E3%83%9E%E3%82%AF%E3%83%AD%E9%96%A2%E9%80%A3)をご確認ください。

## A問題

問題文:

https://atcoder.jp/contests/abc293/tasks/abc293_a

Diff: <font color="Gray">12</font>

どことなく処理内容が複雑な感じではありますが、A問題なだけあって制約が厳しく、問題文通り実装すれば解けます。
`std::swap`を用いて入れ替え処理を行うと簡潔に実装できます。

```cpp
int main() {
    string s;
    cin >> s;
    REP(i,1,s.size()/2){
        swap(s[(i*2)-2],s[2*i-1]);
    }
    cout << s << el;
}
```

提出コード: https://atcoder.jp/contests/abc293/submissions/39603285

## B問題

問題文:

https://atcoder.jp/contests/abc293/tasks/abc293_b

Diff: <font color="Gray">65</font>

A問題と同様、問題文通り実装すればよいです。
ただし、Nの制約が緩くなっているので、毎回「まだ人iが呼ばれていないか」をループで確認するような二重ループ構造にしているとTLEしてしまいます。配列を用いて各人が呼ばれたかを記録しましょう。(下の実装例では`std::set`を用いていますが、[公式解説](https://atcoder.jp/contests/abc293/editorial/5946)の通りbool型の`std::vector`で十分です。)

```cpp
int main() {
    int n;
    cin >> n;
    vector<int> a(n);
    rep(i,0,n) cin >> a[i];
 
    set<int> se;
    rep(i,0,n) se.insert(i+1);
    rep(i,0,n){
        if(se.count(i+1)){
            se.erase(a[i]);
        }
    }
 
    cout << se.size() << el;
    for(auto x : se) cout << x << spa;
    cout << el;
}
```

提出コード: https://atcoder.jp/contests/abc293/submissions/39606575

## C問題

問題文:

https://atcoder.jp/contests/abc293/tasks/abc293_c

Diff: <font color="SaddleBrown">431</font>

$2 \leq H,W \leq 10$ と制約がかなり厳しく、また入出力例2によって$H,W = 10$ で全ての経路を通るケースでも48620通りしか経路がないことがわかります。これは全列挙しても十分間に合いそうなので、どうすれば経路を列挙できるか考えるとよいでしょう。
bit全探索や`std::next_permutation`による順列全探索、幅優先探索などでも解けるようですが、再帰による深さ優先探索(DFS)が直感的な実装方法ではないでしょうか。ということで、実装例では再帰DFSで解いています。ただし自分は再帰DFSが苦手なので、あまりきれいな実装ではないと思います。
実装のお気持ちとしては、以下のような感じです。
+ {0,0}を始点に、「右,下(,前のマスに戻る)」の優先度で進む
+ 経路で整数が重複するかは`set<int>`を用いて判定する
+ {H-1,W-1}のマスにたどり着いた時点でansを+1して前のマスに戻る

dfs関数内の`se2`は、直接`se`に要素を挿入すると一度記録した整数を永続的に保持してしまうため、別途用意した形です。しかし、よく考えれば`se`に要素を挿入しても、dfs関数の終了時(`if(y+1 != h)`の後の部分)に`se.erase(v[y][x])`してやれば問題ありませんでした。[^1]
`se2`を用意する方法では毎回`se`の要素をコピーが生じているので、当然遅くなります。今回は制約が厳しかったので通りましたが、悪い実装方法なことには間違いないので、1つの`std::set`で済ませる方法を強く推奨します。再帰、難しいです……

```cpp
// dfs関数内のコメントはstd::set 1つで済む方法を示す
int h,w;
int ans;
vector<vector<int>> v;

void dfs(int x, int y, set<int>& se){
    if(x+1 == w && y+1 == h) ans++;
    if(x+1 != w){
        if(!se.count(v[y][x+1])){
            set<int> se2 = se;
            se2.insert(v[y][x+1]);
            //se.insert(v[y][x+1]); 上2行は不要.
            dfs(x+1,y,se2);
        }
    }
    if(y+1 != h){
        if(!se.count(v[y+1][x])){
            set<int> se2 = se;
            se2.insert(v[y+1][x]);
            //se.insert(v[y+1][x]); 上2行は不要.
            dfs(x,y+1,se2);
        }
    }
    //se.erase(v[y][x]);
}

//----------------------------------------------------------------------
int main() {
    cin >> h >> w;
  	v.resize(h);
  	rep(i,0,h) v[i].resize(w);
    rep(i,0,h) rep(j,0,w) cin >> v[i][j];

    set<int> se;
    se.insert(v[0][0]);
    dfs(0,0,se);
    cout << ans << el;
}
```

提出コード[^2]: https://atcoder.jp/contests/abc293/submissions/39674406


## D問題

問題文:

https://atcoder.jp/contests/abc293/tasks/abc293_d

Diff: <font color="DarkGreen">830</font>

N本のロープを頂点とし、M回のロープの端を結ぶ操作をロープAとロープCの2頂点間に無向辺を張る と置き換えると、グラフ理論の問題になります。
ここからの判定方法は色々と考えられ、例えば[公式解説](https://atcoder.jp/contests/abc293/editorial/5954)のように連結成分内の全ての頂点の次数が2であるかを確認するなどがあります。この方針だと、[前回(ABC292)のD問題](https://atcoder.jp/contests/abc292/tasks/abc292_d)と同じような実装でよさそうです。[^3]

今回自分がとった方針は、DSU(UnionFind)を用いた閉路判定です。閉路が検出された組は、環状になっているといえます。
A,Cのロープ同士に辺を張る操作M回分をDSUに反映させていきますが、その途中で**辺を張る前からロープAとロープCが同じ組に既に属している**なら閉路を含むということになります。上の解法にもいえますが、色を表すB,Dは使用する必要がないです。

環状の組とそうでない組の個数を出力する必要がありますが、これについては上手な実装方法が思いつきませんでした。実装例では、閉路判定がtrueになったときの頂点番号A,Cを`std::set<int> se`に記録し、[DSU::groups](https://atcoder.github.io/ac-library/production/document_ja/dsu.html)で連結成分ごとに分解したのち(`g`)、各連結成分について`se`に含まれる頂点番号を1つでも含むか判定することで個数をカウントしました。

別解として頂点数2Nのグラフに見立て、頂点番号を`R`なら$2×(A_i-1)$, `B`は$2×(A_i-1)+1$として管理する方法もあります。(Cの場合$C_i$, また`R`と`B`は逆転させてもよい)

```cpp
int main() {
    int n,m;
    cin >> n >> m;
    dsu tree(n);
    set<int> se;
 
    rep(i,0,m){
        int a,c;
        char b,d;
        cin >> a >> b >> c >> d;
        a--, c--;
        if(tree.same(a,c)){
            se.insert(a);
            se.insert(c);
        }
        tree.merge(a,c);
    }
 
    vector<vector<int>> g = tree.groups();
    int alone = g.size();
    for(auto x : g){
        for(auto y : x){
            if(se.count(y)){
                alone--;
                break;
            }
        }
    }
 
    cout << g.size()-alone << spa << alone << el;
}
```

提出コード: https://atcoder.jp/contests/abc293/submissions/39666949

## 感想

A問題もB問題も問題文に少し面食らってしまい、実装が遅れました。どちらもやるだけだったので、もったいなかったです。まぁペナ受けてないので許容範囲ですが……
C問題は苦手な再帰DFSを実装する羽目になり苦労しましたが、一応解けたのでよかったです。ただdfs関数内の[y+1][x]を[y][x+1]と間違えたところがあり、それでペナ合わせると10分弱ロスしたのが痛かったです。添え字には要注意。

それはそうとしてD問題、2分くらいで「グラフ理論の問題に帰着できるな」と確信できたのに、そこから実装も考察も沼って解けなかったのが悔しくて仕方ないです。
グラフ理論の問題はかつて得意だったはずなのに、最近のコンテストでは全然解けていないので重点的に精進しないといけないなと反省しています。別解としてちらりと紹介した「`R`は$2×(A_i-1)$に…」が思い浮かんでいたのですが、これでも上と同じような処理で解けたはずなので、甘かったとしか……
次のコンテストで成功しないと3回目の落茶を経験させられそうなので、次こそは頑張ります。

[^1]: 現在のマスからバックするときに、現在のマスの整数を除外してやればよいだけの話……
[^2]: コンテスト中の提出では、`se2`の使用に加え`ans`と`v`をグローバル変数にしていなかったため、引数も多くなっています。 https://atcoder.jp/contests/abc293/submissions/39623629
[^3]: やってないので本当にできるかはわかりません。多分同じだと思いますが…