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

2019 Normal

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

担当 :  aotsuki

宣伝

  • KMC新入生は・・・無料!!!!!
  • ぜひ皆さん参加しましょう!
  • 日時:5月18日(土)
    • 参加登録締切:5月12日 (日)
    • 今すぐwikiに自分の名前を追加しましょう!

新歓コンパ

自己紹介

自己紹介

  • 京都大学理学部 2回生
  • 本名:宮嶋 優大
  • ​​AtCoder:水色
  • 競プロでは主にC++を使用
  • ゲーム制作とweb開発をほんの少しだけ
  • ボードゲームとか麻雀やったりする
    • 好きな人は今度一緒にやりましょう
  • 楽しくやっていきましょう

KMC-ID : aotsuki

slack(内部チャット)

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

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

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

今日の内容

今日の内容

  • 全探索

    • 深さ優先探索

    • 幅優先探索

    • bit 全探索(おまけ)

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

全探索

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

全探索

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

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

全探索

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

全探索

...

...

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

全探索

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

全探索

線形探索

  • 線形探索とは
    • 全てのデータを順番にforループなどで調べるだけ
    • 単純だがとても重要!
    • みんな書いたことある(はず)!!
    • なので今回は飛ばします...

線形探索

深さ優先探索

  • 深さ優先探索とは
    • 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)を返す。

幅優先探索

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

幅優先探索

O(E)

キューを使ったやり方

サンプルコード

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)を返す。

プログラミング基礎 Ⅲ

ビット

  • ビットとはコンピュータで扱われる情報量の最小単位のこと
  • "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全探索とは
    • bit演算を使って全探索をする
    • ある集合の部分集合を全て列挙することができる
      • "選ぶ", "選ばない"の二択なので   通りを全列挙する
      •       なのでn ≤ 24を目安に使おう

bit全探索

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

例えば、{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 全探索のサンプルコードです

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

とすると復元もできる

コンテスト

  • コンテストはAizu Online Judgeで行います
  • まだしてない人は会員登録をしましょう
  • 下記のURLにアクセスしてください

コンテスト

http://judge.u-aizu.ac.jp/onlinejudge/index.jsp?lang=ja

もしくは

「 AOJ 」で検索

  • 下記のURLにアクセスしてください
  • パスワードは "y" です
  • コンテスト開始までしばしお待ちください

コンテスト

終わり!

解説して欲しいものありますか?

ごはん食べよう!!!

(おごりは資金が底を尽きたため終了しました)

参考文献

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

By procon2019

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

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

  • 454