競技プログラミング練習会

2020 Normal

第4回  全探索, 深さ優先探索, 幅優先探索

担当 :  zeke

自己紹介

自己紹介

  • 京都大学工学部工業化学科 2回生
  • 本名:岡島 和希
  • ​​AtCoder:水色
  • 競プロでは主にC++を使用
  • GoogleCodeJam1C通過できなかったよ…
  • ゲームAI楽しいね
    • 興味があったら声かけてください

KMC-ID : zeke

slack(内部チャット)

#zeke_memoもよろしく!

皆さんの自己紹介もお願いします

  • KMC-ID or 名前
  • 所属(大学など)
  • プログラミング経験や競プロの経験
  • 何か言いたいこと

ささっと、やりましょう!

今日の内容

今日の内容

  • 全探索

    • 順列全探索

    • 深さ優先探索

    • 幅優先探索

    • bit 全探索(おまけ)

  • プログラミング基礎Ⅲ
    • bit 演算

全探索

  • 考えられる全てのパターンを考慮して問題を解く方法
    • ある状態が解であれば、それを解として出力
    • そうでなければ、別の可能性をチェックする
  • 木やグラフを探索するためのアルゴリズムだが、それらに帰着出来ればなんでも使える
  • 「やるだけ」の一種、らしい...

全探索

  • ざっくり言うと...
    • 頂点をある一つの状態、辺を状態の推移とみなし、図示したもの
  • 詳しくは3週間後にやります

  • 考えられる全てのパターンを考慮して問題を解く方法
    • ある状態が解であれば、それを解として出力
    • そうでなければ、別の可能性をチェックする
  • 木やグラフを探索するためのアルゴリズムだが、それらに帰着出来ればなんでも使える
  • 「やるだけ」の一種、らしい...

全探索

  • たいていの事柄はグラフに変換できる!
  • 状態が推移していればOK!
  • 例えば...
    • ○×ゲーム

全探索

...

...

  • 計算量は大きくなることが多い
    • 全ての状態を網羅的に見るため
  • よって、全探索では間に合わないことがある
    • ​オーダーを確認してから全探索を使うこと!
      • ​無理そうなら他の方法を探す...
    • ただ、一部分で全探索することは当然ある

全探索

  • 計算量が大きいので制約が緩いことが多い
  • 比較的簡単?(もちろん難しいのもあります)
  • 実装は重くなることも
  • どんな問題でも全探索解(愚直解)を一度考えてみるのは大事

全探索の問題の特徴と

考え方

  • 全探索のアルゴリズム
    • 線形探索
    • 順列全探索
    • 深さ優先探索
    • 幅優先探索
    • bit 全探索
    • ...

全探索

線形探索

  • 線形探索とは
    • 全てのデータを順番にforループなどで調べるだけ
    • 単純だがとても重要!
    • 意外と気づかない?

線形探索

この前のABC

  • A^5B^5=X  を満たす整数の組(A,B) をひとつ示してください。 ただし、与えられる Xに対して、条件を満たす整数の組 (A,B) が存在することが保証されています。
  • 0<X<1e9+1

この前のABC

  • Aの最大がせいぜい1e3ぐらいなので、Aを-1e3~1e3ぐらいまで試して、Bが整数になるかを調べるだけ!

順列全探索

順列全探索

  • N個の配列の中身の順列を全探索する
  • 最大N!通りある
  • C++にはnext_permutation()という便利なSTLが存在する
  • (他の言語はどうかわかりません(><))
int main() {
    vector<int> vec={1,0,3,2,4};
    sort(vec.begin(),vec.end());//まず配列をソートしましょう
    do{
        for(auto i:vec){//こういう書き方もあります
            cout<<i<<" ";
        }
        cout<<endl;
    }while(next_permutation(vec.begin(),vec.end()));
    //do-while文というものです。whileの中身を実行してから()内を評価します。
}

サンプルコード

  • 典型的な全探索問題だったが、正答率は低かった
  • 実は順列全探索で解くことができる

場合の数

私zekeのACコード(参考)

/*
    Author:zeke

    pass System Test!
    GET AC!!
*/
#include <algorithm>
#include <bitset>
#include <cassert>
#include <cmath>
#include <deque>
#include <functional>
#include <iomanip>
#include <iostream>
#include <map>
#include <queue>
#include <set>
#include <stack>
#include <string>
#include <utility>
#include <vector>
using ll = long long;
using ld = long double;
using namespace std;
#define rep(i, n) for (int i = 0; i < (int)(n); i++)
#define all(x) (x).begin(), (x).end()
#define rep3(var, min, max) for (ll(var) = (min); (var) < (max); ++(var))
#define repi3(var, min, max) for (ll(var) = (max)-1; (var) + 1 > (min); --(var))
#define Mp(a, b) make_pair((a), (b))
#define F first
#define S second
#define Icin(s) \
    ll(s);      \
    cin >> (s);
#define Scin(s) \
    ll(s);      \
    cin >> (s);
template <class T>
bool chmax(T& a, const T& b) {
    if (a < b) {
        a = b;
        return 1;
    }
    return 0;
}
template <class T>
bool chmin(T& a, const T& b) {
    if (b < a) {
        a = b;
        return 1;
    }
    return 0;
}
typedef pair<ll, ll> P;
typedef vector<ll> V;
typedef vector<V> VV;
typedef vector<P> VP;
ll mod = 1e9 + 7;
ll MOD = 1e9 + 7;
ll INF = 1e18;
// cout << "Case #" << index << " :IMPOSSIBLE";
int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    cout << fixed << setprecision(10);
    ll n,m,q;
    cin>>n>>m>>q;
    VV query(q,V(4));
    rep(i,q){
        rep(j,4){
            cin>>query[i][j];
        }
    }
    V vec;
    rep(i,m-1){
        vec.push_back(1);
    }
    rep(i,n){
        vec.push_back(0);
    }
    sort(all(vec));
    ll res=0;
    do{
        ll reg=1;
        ll tempres=0;
        V temp;
        for(auto i:vec){
            if(i==0){
                temp.push_back(reg);
            }else{
                reg++;
            }
        }
        rep(i,q){
            if(temp[query[i][1]-1]-temp[query[i][0]-1]==query[i][2]){
                tempres+=query[i][3];
            }
        }
        chmax(res,tempres);
    }while(next_permutation(all(vec)));
    cout<<res<<endl;
}

next_permutationは重複した配列を返さないことを利用

深さ優先探索

  • 深さ優先探索とは
    • DFS (Depth First Search)とも言う
    • 再帰を使う方法とスタック(stack)を使う方法がある
    • 計算量は   (Eは辺の数)

深さ優先探索

O(E)

再帰を使ったやり方(簡単)

サンプルコード

vector<vector<int> > graph;   //graph[v]には頂点v につながっている
                              //頂点の集合が入っているとする
vector<int> vec;

void dfs(int v){              //v に関して深さ優先探索(vは頂点の番号)
    vec[v]=true;              //v に訪問済みの印を付ける

    /*   v を処理する   */

    for(int i : graph[v]){    //v に接続している各頂点i に対し
        if(vec[i]==false){    //i が未訪問なら
            dfs(i);           //i に関して深さ優先探索
        }
    }
}

スタックを使ったやり方

サンプルコード

void dfs(int z){                  //z から始める深さ優先探索(zは頂点の番号)
    stack<int> S;                 //空のスタックS を用意
    vec[z] = true;                //z に訪問済みの印を付ける
    S.push(z);                    //z をスタックS に積む

    while(!S.empty()){            //スタックS が空でなければ
        int v = S.top();          //スタックS の一番上にある要素を取り出す
        S.pop();                  //スタックS の一番上にある要素を削除

        /*   v を処理する   */

        for(int i : graph[v]){    //v に接続している各頂点i に対し
            if(vec[i]==false){    //i が未訪問なら
                vec[i] = true;    //i に訪問済みの印を付ける
                S.push(i);        //i をスタックS に積む
            }
        }
    }
}

stackの概要

  • Last In, First Out(LIFO)で、最後に入れたものを最初に処理するデータ構造
  • あんまり使わないかも…(vectorの下位互換みたいなところもあるし)
  • 次回やる深さ優先探索で使えるが、再帰で書くことが多い。

stackの関数

  • 以下stをstackとする。
  • 全てO(1)
  • st.push(x);
    • stの末尾にxを追加
  • st.pop();
    • stの末尾の要素を削除
  • st.top()
    • stの末尾の要素を取得
  • st.empty()
    • stが空ならtrue(1),そうでないならfalse(0)を返す。

とにかく

  • 再帰関数をマスターしよう!!!
  • さっき出した問題はDFSでも解けるよ~という話
vector<vector<ll>> vec;
ll N, M, Q;
ll res=0;
void dfs(string s){
    if(s.size()==N+1){
        ll tempres=0;
        rep(i,Q){
            if ((s[vec[i][1]]-'0')-(s[vec[i][0]]-'0')==vec[i][2]) {
                tempres += vec[i][3];
            }
        }
        chmax(res,tempres);
        return;
    }
    for(int i=s.back()-'0';i<=M;i++){
        dfs(s+(char)(i+'0'));
    }
}
int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    cout << fixed << setprecision(10);
    cin>>N>>M>>Q;
    vec.resize(Q,vector<ll>(4));
    rep(i,Q){
        rep(j,4){
            cin>>vec[i][j];
        }
    }
    dfs("1");
    cout<<res<<endl;
}

幅優先探索

  • 幅優先探索とは
    • BFS (Breadth First Search)とも言う
    • キュー(queue)を用いる
    • 計算量は   (Eは辺の数)

幅優先探索

O(E)

BFSの流れ

  • queueに何か値を放り込む
  • queueの中身がなくなるまで繰り返す
    • queueの中身を取り出す
    • 取り出したやつをqueueから削除
    • なんか処理してqueueに何かを追加したりする

キューを使ったやり方

サンプルコード

void bfs(int z){                  //z から始める幅優先探索(zは頂点の番号)
    queue<int> Q;                 //空のキューQ を用意
    vec[z] = true;                //z に訪問済みの印を付ける
    Q.push(z);                    //z をキューQ に追加

    while(!S.empty()){            //キューQ が空でなければ
        int v = Q.front();        //キューQ の一番上にある要素を取り出す
        Q.pop();                  //キューQ の一番上にある要素を削除

        /*   v を処理する   */

        for(int i : graph[v]){    //v に接続している各頂点i に対し
            if(vec[i]==false){    //i が未訪問なら
                vec[i] = true;    //i に訪問済みの印を付ける
                S.push(i);        //i をキューQ に追加
            }
        }
    }
}

queueの概要

  • First In, First Out(FIFO)で最初に入れたものを最初に処理するデータ構造
  • 次回やる、幅優先探索で使います。

queueの関数

  • 以下qをqueueとする。
  • 全てO(1)
  • q.push(x);
    • qの末尾にxを追加する。
  • q.pop();
    • qの先頭の要素を削除する。
  • q.front()
    • qの先頭の要素を取得する。
  • q.empty()
    • qが空ならtrue(1),そうでないならfalse(0)を返す。

BFSでも(ry

vector<vector<ll>> vec;
ll N, M, Q;
ll res=0;
int main() {
    cin.tie(0);
    ios::sync_with_stdio(false);
    cout << fixed << setprecision(10);
    cin>>N>>M>>Q;
    vec.resize(Q,vector<ll>(4));
    rep(i,Q){
        rep(j,4){
            cin>>vec[i][j];
        }
    }
    queue<string> q;
    q.push("1");
    while(!q.empty()){
        string s=q.front();
        q.pop();
        if(s.size()==N+1){
            ll tempres=0;
            rep(i,Q){
                if ((s[vec[i][1]]-'0')-(s[vec[i][0]]-'0')==vec[i][2]) {
                    tempres += vec[i][3];
                }
            }
            chmax(res,tempres);
            continue;
        }
        for(int i=s.back()-'0';i<=M;i++){
            q.push(s+(char)(i+'0'));
        }
    }
    cout<<res<<endl;
}

全探索系での注意

注意点

  • 全部見ることを意識すること
  • 同じものを何度も見ないようにすること
    • 先ほどの迷路だと何度も同じ経路をたどったりすること
    • 無限ループとなりTLEがでる
    • 防ぐには、もう見たという「印」をつける

プログラミング基礎 Ⅲ

ビット

  • ビットとはコンピュータで扱われる情報量の最小単位のこと
  • "binary digit"を略したものが語源になっているといわれている
  • ビットは2進数で表現され、2進数の1桁 = 1ビット

バイト

  • バイトとはコンピュータで扱われる情報量の単位のこと
  • 1バイトは8ビットであらわす事ができる
  • 1バイトで最大256個の状態(数値)を表現することができる

2進数表記

  • 数値の先頭に0bをつけると2進数表記になる
  • 0xを付けると16進数、0を付けると8進数になる

例:142         2進数     0b10001110

        16進数  0x8E

          8進数     0216

補数

  • 補数とはある値に加えると一定の基準値になる値のこと
  • 基準①:その桁数で最大の数
  • 基準②:桁が一つ繰り上がる数

基準 よって求められた値を2進数では『1の補数』

基準 よって求められた値を2進数では『2の補数』

という

補数

b進法において、自然数aを表現するのに必要な最小の桁数をnとしたとき、

  • b^n - a を、b進法におけるaに対する基数の補数 (bの補数)
  • b^n - a - 1 を、b進法におけるaに対する減基数の補数(b-1の補数)

という

(詳しく書くと...)

補数

1の補数

「足しても桁上がりしない数のうち最大の数」

  (ここでいう桁とは演算範囲の桁数を指す )

 

補数

1の補数

「足しても桁上がりしない数のうち最大の数」

  (ここでいう桁とは演算範囲の桁数を指す )

 

 例:0b01110100  0b10001011

  (0b01110100 + 0b 10001011 = 0b11111111)

 

補数

1の補数

「足しても桁上がりしない数のうち最大の数」

  (ここでいう桁とは演算範囲の桁数を指す )

 

 例:0b01110100  0b10001011

  (0b01110100 + 0b 10001011 = 0b11111111)

 

 =>各ビットを反転させたもの

補数

2の補数

桁が一つ繰り上がる数

   (ここでいう桁とは演算範囲の桁数を指す )

 

補数

2の補数

桁が一つ繰り上がる数

   (ここでいう桁とは演算範囲の桁数を指す )

 

 例:0b01110100  0b10001100

  (0b01110100 + 0b 10001100 = 0b100000000)

 

補数

2の補数

桁が一つ繰り上がる数

   (ここでいう桁とは演算範囲の桁数を指す )

 

 例:0b01110100  0b10001100

  (0b01110100 + 0b 10001100 = 0b100000000)

 

 =>各ビットを反転させ、1を足したもの

 

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

 

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

     →0b11110010

 

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

     →0b11110010

 

    14 + (-14) = 

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

     →0b11110010

 

    14 + (-14) = 0b00001110

                     + 0b11110010

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

     →0b11110010

 

    14 + (-14) = 0b00001110

                     + 0b11110010

                      = 0b100000000

2進数による負の値の表現

コンピュータで2進数の負の値を表すときは絶対値の「2の補数」を使用して表現する

例: -14

    |-14|=14 (0b00001110)の2の補数 

     →ビット反転  + 1   (0b11110001 + 1)

     →0b11110010

 

    14 + (-14) = 0b00001110

                     + 0b11110010

                      = 0b100000000

                    → 0   ( 演算範囲は8桁だからあふれた9桁目は無視)

ビット演算

ビット演算とは

  • 各のビット全てに対する論理演算をいっぺんに行う演算操作

 

  • いろんなところで使える
  • Binary Indexed Tree (BIT)
  • bit 全探索
  • bit DP
  • ...

ビット演算の利点

  • データ量が少なくて済む
  • 多少速い
  • 一つのデータに複数の情報を詰め込める(フラグ管理など)

ビット演算子の種類

  • NOT  (否定)                        ~x 
  • AND  (論理積)                    x & y
  • OR  (論理和)                       x | y
  • XOR  (排他的論理和)         x ^ y
  • 右シフト                             x >> n
  • 左シフト                             x << n

NOT  (否定)        ~x 

x 0 1 1 0 0 1 0 1
~x 1 0 0 1 1 0 1 0
  • 1の補数
  • ビットを反転させる(0→1/1 0)

AND  (論理積)    x&y 

x 1 0 0 1 1 1 0 0
y 1 1 0 0 1 0 1 0
x&y 1 0 0 0 1 0 0 0
  • 両方のビットが1の時、結果が1になる

OR  (論理和)    x|y 

  • どちらかのビットが1の時、結果が1になる
x 1 0 0 1 1 1 0 0
y 1 1 0 0 1 0 1 0
x|y 1 1 0 1 1 1 1 0

XOR  (排他的論理和)    x ^ y

  • 互いのビットが不一致の時、結果が1になる
x 1 0 0 1 1 1 0 0
y 1 1 0 0 1 0 1 0
x^y 0 1 0 1 0 1 1 0

右シフト    x >> n

  • 各ビットを右にn個ずらす
  •    と 同じ
x 1 0 0 1 1 1 0 0
x>>n 0 0 1 0 0 1 1 1

{

{

n=2 

x : unsigned char

空いた桁は0で埋まる

このbitは捨てられる

x/{2^n}

(ほぼ)

左シフト    x << n

x 1 0 0 1 1 1 0 0
x<<n 0 1 1 1 0 0 0 0

{

{

n=2 

x : unsigned char

空いた桁は0で埋まる

このbitは捨てられる

  • 各ビットを左にn個ずらす
  • 桁があふれなければ   と 同じ
{x}*{2^n}

(以下参考)

※注意点

シフト演算子は優先順位が低いので注意

  • あやふやな時は( )を付ける!    

 

詳しくはここを参照

※注意点

シフトには2種類ある

※注意点

シフトには2種類ある

・算術シフト

シフトによって空いたビット部分を符号ビットを同じもので詰める

・論理シフト

シフトによって空いたビット部分を常に0で詰める

※注意点

シフトには2種類ある

・算術シフト

シフトによって空いたビット部分を符号ビットを同じもので詰める

・論理シフト

シフトによって空いたビット部分を常に0で詰める

符号が正のときはどちらも同じ

※注意点

シフトには2種類ある

・算術シフト

シフトによって空いたビット部分を符号ビットを同じもので詰める

・論理シフト

シフトによって空いたビット部分を常に0で詰める

符号が正のときはどちらも同じ

=>問題は符号が負のとき

※注意点

・論理シフト

x 1 0 0 1 1 1 0 0
x>>n 0 0 1 0 0 1 1 1

n=2 

x : unsigned char

空いた桁は0で埋まる

{

x 1 0 0 1 1 1 0 0
x<<n 0 1 1 1 0 0 0 0

※注意点

・算術シフト

x 1 0 0 1 1 1 0 0
x>>n 1 0 0 1 1 1

n=2 

x : signed char

空いた桁は  で埋まる

{

x 1 0 0 1 1 1 0 0
x<<n 1 1 1 0 0 0 0

=>常に符号は変わらない

※注意点

これらはコンパイラによって決まる

gccでは

算術シフト  signed int/long/long long

論理シフト unsigned int/long/long long

予期せぬ挙動をすることがあるので注意!

bit に i 番目のフラグが立っているか
(0-indexed)
if (bit & (1<<i))
bit に i 番目のフラグが消えているか if (!(bit & (1<<i)))
bit に i 番目のフラグを立てる bit|= (1<<i)
bit に i 番目のフラグを消す bit &= ~(1<<i)
bit に何個のフラグが立っているか __builtin_popcount(bit) (int)
__builtin_popcountl(bit) (long)
__builtin_popcountll(bit) (long long)
小さい方から何桁目に初めて立ったフラグが現れるか
(1-indexed)
__builtin_ffs(bit) (int)
__builtin_ffsl(bit) (long)
__builtin_ffsll(bit) (long long)

フラグ管理

std::bitset を用いた出力

#include <iostream>
#include <bitset>
using namespace std;

int main() {
    int A = 0x2d;
    int B = 0x19;
    cout << bitset<8>(A) << " AND " << bitset<8>(B) << " = " << bitset<8>(A&B) << endl;
}
00101101 AND 00011001 = 00001001

出力

std::bitsetを使うと2進数で出力できる(デバッグでよく使う)

よいサイト

bit全探索

bit全探索を始める前に

  • はっきり言って初心者には難しい
  • 実装は今できなくてもいいのでイメージだけでも覚えておくとOK
  • bit全探索とは
    • bit演算を使って全探索をする
    • ある集合の部分集合を全て列挙することができる
      • "選ぶ", "選ばない"の二択なので   通りを全列挙する
      •       なのでn ≤ 24を目安に使おう

bit全探索

2^n
2^{24}>10^7

bit全探索のイメージ

1 2 3 4 5 6 7 8

1~8と書かれたボールがあって箱に入れる場合の数

例えば、{A,B,C}という集合があるとする

bit全探索

10進数 2進数 部分集合
0 000 xxx
1 001 xxA
2 010 xBx
3 011 xBA
4 100 Cxx
5 101 CxA
6 110 CBx
7 111 CBA

1を足していくだけで部分集合の全列挙が可能!

2進数の0,1,2桁にA,B,Cを対応させると...

bitはN個の要素の集合を整数で表すことができるーーー!!!!!!!!

つまり

サンプルコード

bit 全探索のサンプルコードです

vector<int> vec;
int n = vec.size();

for(int bit=0;bit<((int)1<<n);bit++){

    for(int i=0;i<n;i++){
        if(((int)1<<i)&bit){

            /*   処理を書く   */
        }
    }

            /*   処理を書く   */
}

サンプルコード解説

vector<int> vec;
int n = vec.size();

for(int bit=0;bit<((int)1<<n);bit++){

    for(int i=0;i<n;i++){
        if(((int)1<<i)&bit){

            /*   処理を書く   */
        }
    }

            /*   処理を書く   */
}

"1<<n" は1をnだけ左シフトする

=>つまり、 と同じ!

2^n

サンプルコード解説

vector<int> vec;
int n = vec.size();

for(int bit=0;bit<((int)1<<n);bit++){

    for(int i=0;i<n;i++){
        if(((int)1<<i)&bit){

            /*   処理を書く   */
        }
    }

            /*   処理を書く   */
}

 "1<< i" はi桁目のビットのみ1

"&" は各ビットについて共に1なら1

"(1<< i)&bit" => bit のi桁目が1なら真(0以外)になる

おまけ

2変数を同時に保持する方法の一つとしてpairがあるが、他の方法もある

2値のデータの保持

例えば、10×10のxy座標があるとする

  z = y × 10 + x

とおくと変数一つで x, y の情報を保持できる

  x = z % 10

  y = z  /  10

とすると復元もできる

二次元情報を一次元に移すことができるので便利!

ゲームAIについて

(戯言)

  • 基本的に二人零和有限確定完全情報ゲームなどは全探索アルゴリズムを使用することがある
  • 競プロでも、ゲーム問題のカテゴリが存在する
  • よかったら去年の僕のスライドを見てみてね!(宣伝)

終わり!

参考文献

Copy of 第4回:全探索, 深さ優先探索, 幅優先探索

By kmc_procon2020

Copy of 第4回:全探索, 深さ優先探索, 幅優先探索

発表日時 2019年5月11日(金) 18:30-21:00 https://onlinejudge.u-aizu.ac.jp/beta/room.html#kmc2019_n_4

  • 178