NPCA合宿きょーぷろ講義

65bd3434deeeb8afc00485cdfc3e260a?s=47 snuke
August 22, 2013

 NPCA合宿きょーぷろ講義

65bd3434deeeb8afc00485cdfc3e260a?s=128

snuke

August 22, 2013
Tweet

Transcript

  1. きょーぷろ講義 in NPCA summer camp by snuke

  2. 競技プログラミングとは アルゴリズムの力を競うゲーム

  3. アルゴリズムとは 手順を工夫して、ムダを省くこと

  4. 前半の内容 DP

  5. DPとは Dynamic Programing の略 別名、動的計画法

  6. なんか難しそう? 否、DP自体はカンタンなのだ!

  7. でも いろんなバリエーションがあって、 応用力・経験・センスが問われます そこが DP の難しいところなのです この講義での目的は、考え方の基礎 をしっかりと固めることです

  8. DPとは 典型的な例を見てみましょう

  9. DPの例 左下から右上まで、右or上に線を辿 って行く方法は何通り? S T

  10. 小学生でも解ける解法 交点に数字を書いていく S T

  11. 小学生でも解ける解法 交点に数字を書いていく S T 1

  12. 小学生でも解ける解法 交点に数字を書いていく S T 1 1

  13. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1

  14. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1

  15. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1

  16. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

  17. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3
  18. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3 4
  19. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3 4 1
  20. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3 4 1 3
  21. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3 4 1 3 6
  22. 小学生でも解ける解法 交点に数字を書いていく S T 1 1 1 1 1 2

    3 4 1 3 6 10
  23. 小学生でも解ける解法 この解法を考察してみる S T 1 1 1 1 1 2

    3 4 1 3 6 10
  24. 小学生でも解ける解法 交点だと数字が見にくいので、 とりあえずマス目にしときます 1 1 1 1 1 2 3

    4 1 3 6 10 S T 0 1 2 3 0 1 2 i j
  25. 小学生でも解ける解法 各マスの値が表しているのは 「Sからそのマスへ行く方法の個数」 1 1 1 1 1 2 3

    4 1 3 6 10 S T 0 1 2 3 0 1 2 i j
  26. 小学生でも解ける解法 真上のマスの値と左隣のマスの値の 合計を書き込んでいく 1 1 1 1 1 2 3

    S T 0 1 2 3 0 1 2 i j
  27. 小学生でも解ける解法 真上のマスと左隣のマスにはすでに 数字が書き込まれていないとダメ 1 1 1 1 1 2 3

    ? ? S T 0 1 2 3 0 1 2 i j
  28. 小学生でも解ける解法 うまい順序で埋めて行けば良い → 分かるところから順番に 1 1 1 1 1 2

    3 S T 0 1 2 3 0 1 2 i j
  29. 小学生でも解ける解法 これが、DPなのです! 1 1 1 1 1 2 3 S

    T 0 1 2 3 0 1 2 i j
  30. DPとは 表を作って 分かるところから順番に ルールに従って埋めて行く

  31. DPとは 表を作って → (i,j)は「Sからマス(i,j)へ行く方法の個数」 分かるところから順番に → 1列ずつ順番に、など ルールに従って埋めて行く → 上+左

    の値を書き込む
  32. DPとは どんな表を作るかが勝負の鍵!

  33. 部分和問題 N 個の数字 a1,a2....aN からいくつ かの数字を選んで、和が X になるよ うに出来るだろうか?

  34. 例 N = 4 a = {1,4,3,3} X = 10

    → 4 + 3 + 3 = 10 できる!
  35. 例 N = 4 a = {1,4,3,3} X = 7

    → 1 + 3 + 3 = 4 + 3 = 7 できる!
  36. 例 N = 4 a = {1,4,3,3} X = 9

    → できぬ
  37. 表の作り方 (i,j) = a1からaiまでの数字を使っ     て j を作れるかどうか

  38. 実際にやってみる N = 4 a = {1,4,3,3} X = 9

  39. 目標 o x x x x x x x x

    x o o x x x x x x x x o o x o o x x x x x o o x o o o x o o x o o x o o o o o o x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9 a = {1 4 3 3}
  40. まずは初期化 o x x x x x x x x

    x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9
  41. a1 = 1 足す/足さないの二択 o x x x x x

    x x x x o o x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9
  42. a2 = 4 o x x x x x x

    x x x o o x x x x x x x x o o x x o o x x x x x x x x x x x x x x x x x x x x x x x x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9
  43. a3 = 3 o x x x x x x

    x x x o o x x x x x x x x o o x x o o x x x x o o x o o o x o o x x x x x x x x x x x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9
  44. a4 = 3 o x x x x x x

    x x x o o x x x x x x x x o o x x o o x x x x o o x o o o x o o x o o x o o o o o o x 0 1 2 3 0 1 2 i j 3 4 4 5 6 7 8 9
  45. 実装しよう 表は配列で表現する

  46. 実装しよう 入力: 1行目に N と X が空白区切りで書かれている 2行目に a1...aNが空白区切りで書かれている 4

    9 1 4 3 3 NO 入力 出力
  47. 実装 #include<stdio.h> int main(){ return 0; } #include<cstdio> using namespace

    std; int main(){ return 0; } C C++
  48. 入力 #include<stdio.h> int main(){ int i, j; int n, x;

    scanf(“%d%d”,&n,&x); int a[105]; for(i = 1; i <= n; i++) scanf(“%d”,&a[i]); return 0; } #include<cstdio> using namespace std; int main(){ int n, x; scanf(“%d%d”,&n,&x); int a[105]; for(int i = 1; i <= n; i++) scanf(“%d”,&a[i]); return 0; } 4 9 1 4 3 3
  49. DPテーブルの初期化 #include<stdio.h> int dp[105][11005]; int main(){ int i, j; int

    n, x; scanf(“%d%d”,&n,&x); int a[105]; for(i = 1; i <= n; i++) scanf(“%d”,&a[i]); for(i = 0; i <= n; i++) for(j = 0; j <= x; j++) dp[i][j] = 0; dp[0][0] = 1; return 0; } #include<cstdio> using namespace std; bool dp[105][11005]; int main(){ int n, x; scanf(“%d%d”,&n,&x); int a[105]; for(int i = 1; i <= n; i++) scanf(“%d”,&a[i]); for(int i = 0; i <= n; i++) for(int j = 0; j <= x; j++) dp[i][j] = false; dp[0][0] = true; return 0; }
  50. かなめの部分 ུ for(i = 0; i <= n; i++) for(j

    = 0; j <= x; j++) dp[i][j] = 0; dp[0][0] = 1; for(i = 1; i <= n; i++){ for(j = 0; j <= x; j++){ } } return 0; } ུ for(int i = 0; i <= n; i++) for(int j = 0; j <= x; j++) dp[i][j] = false; dp[0][0] = true; for(int i = 1; i <= n; i++){ for(int j = 0; j <= x; j++){ } } return 0; }
  51. かなめの部分 ུ for(i = 1; i <= n; i++){ for(j

    = 0; j <= x; j++){ if(dp[i-1][j]){ dp[i][j] = 1; dp[i][j+a[i]] = 1; } } } if(dp[n][x] == 1) puts(“YES”); else puts(“NO”); return 0; } ུ for(int i = 1; i <= n; i++){ for(int j = 0; j <= x; j++){ if(dp[i-1][j]){ dp[i][j] = true; dp[i][j+a[i]] = true; } } } if(dp[n][x]) puts(“YES”); else puts(“NO”); return 0; }
  52. 完成! #include<stdio.h> int dp[105][11005]; int main(){ int i, j; int

    n, x; scanf(“%d%d”,&n,&x); int a[105]; for(i = 1; i <= n; i++) scanf(“%d”,&a[i]); for(i = 0; i <= n; i++) for(j = 0; j <= x; j++) dp[i][j] = 0; dp[0][0] = 1; for(i = 1; i <= n; i++){ for(j = 0; j <= x; j++){ if(dp[i-1][j] == 1){ dp[i][j] = 1; dp[i][j+a[i]] = 1; } } } if(dp[n][x] == 1) puts(“YES”); else puts(“NO”); return 0; } #include<cstdio> using namespace std; bool dp[105][11005]; int main(){ int n, x; scanf(“%d%d”,&n,&x); int a[105]; for(int i = 1; i <= n; i++) scanf(“%d”,&a[i]); for(int i = 0; i <= n; i++) for(int j = 0; j <= x; j++) dp[i][j] = false; dp[0][0] = true; for(int i = 1; i <= n; i++){ for(int j = 0; j <= x; j++){ if(dp[i-1][j]){ dp[i][j] = true; dp[i][j+a[i]] = true; } } } if(dp[n][x]) puts(“YES”); else puts(“NO”); return 0; }
  53. DPのバリエーション 表の作り方 例:フィボナッチ数列 → dp[i] = i番目の数字 多次元配列になることも多い 例:さっきの問題に「ちょうど M

    個の数字を使 う」っていう条件が加わったら、 dp[i][j][k] = a1..ai から j 個の数字を使って        和を k にできるかどうか 集合を要素にしたり、他にもいろいろ・・・
  54. DPのバリエーション 埋め方のルール 難しい問題になると、ここが複雑だったり、 ここ工夫しないといけなかったりもする

  55. 休憩 暇な人はこの問題でも実装してみて 問題: N×Mの格子の左下から右上まで 遠回りせずに行く方法は何通り? 入力: N と M が空白区切りで入力される

    1 <= N,M <= 16 (答えはint型に収まります)
  56. 後半の内容 άϥϑ

  57. グラフとは 頂点 を 辺 でつないだもの

  58. グラフとは たとえばこんなの 2 1 3 4 5

  59. グラフとは ものどうしの関係を表す(例:facebookの友人関係) 2 1 3 4 5 もの 関係 (頂点)

    (辺)
  60. グラフとは これらは全く同じグラフ 見た目はどうでも良く、つながり方だけが重要 2 1 3 4 5 2 1

    3 5 4 2 1 3 4 5
  61. 遊び 間違い探し - 数字の書いていない頂点どうしは区別しない

  62. 遊びの答え         

          
  63. 遊びの答え         

          
  64. 遊びの答え 2 2 1 1 1 2 2 1 1

    2 2 1 3 1 1 1
  65. 遊びの答え 4 2 3 3 2 4 3 2 3

    2 4 3 2 2 3 2 3 4 3 2 2 2 4 4 2
  66. 遊びの答え

  67. 有向グラフとは 辺に向きがあるグラフ(例:twitterのフォロー関係) ちなみにさっきまでのグラフは 無向グラフ 2 1 3 4 5

  68. ループとは こんなの 別名 閉路 とも言う。

  69. DAG とは Directed acyclic graph の略 ループのない有向グラフ

  70. DAGの何が嬉しい? DAGは頂点に ॱং෇͚͕Ͱ͖Δ

  71. アルゴリズムとは 手順を工夫して、ムダを省くこと

  72. アルゴリズムとは  खॱを工夫して、ムダを省くこと

  73. 順序付け? 0 2 1 4 3 ʢτϙϩδΧϧιʔτʣ こんな感じ - 右向きの辺のみにできる

  74. 遊び2 有向グラフの頂点に数字を書き込ん で、大→小の辺がないようにしよう できない場合はループを見つけよう ループがあったらDAGではない

  75. 遊び2の答え 2 3 4 5 1 2 3 1 4

    1 4 3 2
  76. 遊び2の答え 1 5 3 2 4 1 4 2 5

    3
  77. 遊び2の答え 9 2 5 8 7 1 4 3 6

  78. 順序付けができると? 例題: DAGの頂点Sから頂点Tまでの 最長経路の長さを求めよ。 S T

  79. 例題: DAGの頂点Sから頂点Tまでの 最長経路の長さを求めよ。 S T 順序付けができると?

  80. とりあえず 順序づけた形にしておきます S T 0 2 4 3 1

  81. 解き方 0 2 4 3 1 最長距離 頂点0からの最長距離を記録する表を 用意します

  82. 解き方 0 2 4 3 1 0 ? ? ?

    ? 最長距離 最初はこんな感じ
  83. 解き方 0 2 4 3 1 0 1 1 ?

    ? 最長距離 頂点0から1本の辺を辿って行ける頂 点の値を更新します
  84. 解き方 0 2 4 3 1 0 1 1 ?

    2 最長距離 頂点1に入ってくる辺は全て試したの で、頂点1の値は確定しました さっきと同じように更新していきます
  85. 解き方 0 2 4 3 1 0 1 1 2

    2 最長距離 頂点2の値が確定しました
  86. 解き方 0 2 4 3 1 0 1 1 2

    3 最長距離 すでに数字が書かれている時は、書か れている数字よりも大きくなる時だけ 更新します
  87. 解き方 0 2 4 3 1 0 1 1 2

    3 最長距離 できた! 答えは3! やったね!
  88. 解き方 0 2 4 3 1 0 1 1 2

    3 最長距離 要するに DP です!
  89. 今のを実装してみよう 頂点数が N の順序付けされたDAGが 与えられるので、頂点0から頂点N-1 までの最長距離を出力せよ。 0 2 4 3

    1 入力 3 出力
  90. 実際の入力の例 5 6 0 1 0 2 1 4 2

    3 2 4 3 4 0 2 4 3 1 頂点の数 辺の数 辺の情報 0→1っていう辺が あるっていう意味
  91. グラフの実装 vector<int> to[頂点数];

  92. vectorってなんやねん 動的配列 = 大きさを自由に変えられる配列

  93. vectorの使用例 int i; vector<int> a; for(i = 0; i <

    3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  94. シミュレーション int型の動的配列を作る。(まだ空っぽ) int i; vector<int> a; for(i = 0; i

    < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); } a: 空っぽ
  95. シミュレーション a の一番後ろに i+5 を追加 a: 5 int i; vector<int>

    a; for(i = 0; i < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  96. シミュレーション ループが1回回って i = 1、末尾に i+5 を追加 a: 5 6

    int i; vector<int> a; for(i = 0; i < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  97. シミュレーション ループが回って i = 2、末尾に i+5 を追加 a: 5 6

    7 int i; vector<int> a; for(i = 0; i < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  98. シミュレーション a.size() は「a に入ってる要素の数」を表す a: 5 6 7 int i;

    vector<int> a; for(i = 0; i < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  99. シミュレーション 配列みたいに添字でアクセスできる a: 5 6 7 int i; vector<int> a;

    for(i = 0; i < 3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); }
  100. 配列と見比べてみる int i; vector<int> a; for(i = 0; i <

    3; i++){ a.push_back(i+5); } for(i = 0; i < a.size(); i++){ printf(“%d\n”,a[i]); } int i; int a[3]; for(i = 0; i < 3; i++){ a[i] = i+5; } for(i = 0; i < 3; i++){ printf(“%d\n”,a[i]); } ほぼ同じような感じ
  101. vectorの何が嬉しい? 必要なだけのサイズの配列が取れる 必要なサイズがあらかじめ決まっている場合は配列で良い 配列の要素数を簡単に調べられる 「.size()」をつけるだけ など

  102. で、グラフの実装に戻る vector<int> to[頂点数]; 例えば、v→uっていう辺があったら  to[v].push_back(u); みたいに追加する

  103. こんな感じになる 0 2 4 3 1 1 2 0 1

    2 3 4 3 4 4 4 5 6 0 1 0 2 1 4 2 3 2 4 3 4 to
  104. じゃあ実装してみよう でも vector は C にはない! C++ にしかない! → C++

    を使おう!
  105. don’t worry C から C++ の移行はカンタン → ほとんど同じように使えるから しかも、たくさんの便利な機能が使 えるようになる!

  106. Hello world! ほとんどいっしょだね! #include<stdio.h> int main(){ printf(“Hello world!”); return 0;

    } #include<cstdio> using namespace std; int main(){ printf(“Hello world!”); return 0; }
  107. C++とCでちょっと違うところ includeするもの → 例: stdio.h -> cstdio → 一応 stdio.h

    でも問題ない using namespace std; → 最初はなんにも考えずにとりあえずおまじな いとして書いとけば良い
  108. 今からしてもらうこと ちょっと脱線しまくったのでまとめておきます DAGの最長距離を求めるプログラム vectorを使ってグラフを処理する vectorを使うために C++ を使う

  109. さあ実装開始 #include<cstdio> using namespace std; int main(){ return 0; }

    とりあえず大枠
  110. 頂点数と辺数の入力 #include<cstdio> using namespace std; int main(){ int n, m;

    scanf(“%d%d”,&n,&m); return 0; } n = 頂点数 m = 辺数 多くの問題ではこんな 変数名が付いている
  111. グラフの入力 #include<cstdio> #include<vector> using namespace std; int main(){ int n,

    m; scanf(“%d%d”,&n,&m); vector<int> to[1005]; for(int i = 0; i < m; i++){ } return 0; } n = 頂点数 m = 辺数 to = グラフ
  112. グラフの入力 #include<cstdio> #include<vector> using namespace std; int main(){ int n,

    m; scanf(“%d%d”,&n,&m); vector<int> to[1005]; for(int i = 0; i < m; i++){ int v, u; scanf(“%d%d”,&v,&u); to[v].push_back(u); } return 0; } n = 頂点数 m = 辺数 to = グラフ
  113. 最長距離リストの初期化 ུ vector<int> to[1005]; for(int i = 0; i <

    m; i++){ int v, u; scanf(“%d%d”,&v,&u); to[v].push_back(u); } int dist[1005]; for(int i = 0; i < n; i++) dist[i] = -1000000000; dist[0] = 0; return 0; } n = 頂点数 m = 辺数 to = グラフ dist = 距離
  114. かなめの部分 ུ int dist[1005]; for(int i = 0; i <

    n; i++) dist[i] = -1000000000; dist[0] = 0; for(int v = 0; v < n; v++){ } return 0; } n = 頂点数 m = 辺数 to = グラフ dist = 距離 0 2 4 3 1 0 1 1 ? ? 0 2 4 3 1 0 1 1 2 2
  115. かなめの部分 ུ for(int v = 0; v < n; v++){

    for(int i = 0; i < to[v].size(); i++){ } } return 0; } n = 頂点数 m = 辺数 to = グラフ dist = 距離 0 2 4 3 1 0 1 1 ? ? 0 2 4 3 1 0 1 1 2 2
  116. かなめの部分 for(int v = 0; v < n; v++){ for(int

    i = 0; i < to[v].size(); i++){ int u = to[v][i]; dist[u] = max(dist[u], dist[v]+1); } } printf(“%d\n”,dist[n-1]); n = 頂点数 m = 辺数 to = グラフ dist = 距離 max(x,y):大きい方を返す関数 algorithmをincludeすると使える #include<algorithm>
  117. 完成! #include<cstdio> #include<vector> #include<algorithm> using namespace std; int main(){ int

    n, m; scanf(“%d%d”,&n,&m); vector<int> to[1005]; for(int i = 0; i < m; i++){ int v, u; scanf(“%d%d”,&v,&u); to[v].push_back(u); } int dist[1005]; for(int i = 0; i < n; i++) dist[i] = -1000000000; dist[0] = 0; for(int v = 0; v < n; v++){ for(int i = 0; i < to[v].size(); i++){ int u = to[v][i]; dist[u] = max(dist[u], dist[v]+1); } } printf(“%d\n”,dist[n-1]); return 0; }
  118. おまけ その1:最長経路ではなく最短経路を求める その2:異なる経路の個数を求める いずれもさっきのをちょっと変えるだけで出来る

  119. おまけの答え #include<cstdio> #include<vector> #include<algorithm> using namespace std; int main(){ int

    n, m; scanf(“%d%d”,&n,&m); vector<int> to[1005]; for(int i = 0; i < m; i++){ int v, u; scanf(“%d%d”,&v,&u); to[v].push_back(u); } int dist[1005]; for(int i = 0; i < n; i++) dist[i] = 1000000000; dist[0] = 0; for(int v = 0; v < n; v++){ for(int i = 0; i < to[v].size(); i++){ int u = to[v][i]; dist[u] = min(dist[u], dist[v]+1); } } printf(“%d\n”,dist[n-1]); return 0; } #include<cstdio> #include<vector> #include<algorithm> using namespace std; int main(){ int n, m; scanf(“%d%d”,&n,&m); vector<int> to[1005]; for(int i = 0; i < m; i++){ int v, u; scanf(“%d%d”,&v,&u); to[v].push_back(u); } int count[1005]; for(int i = 0; i < n; i++) count[i] = 0; count[0] = 1; for(int v = 0; v < n; v++){ for(int i = 0; i < to[v].size(); i++){ int u = to[v][i]; count[u] = (count[u]+count[v])%1000000009; } } printf(“%d\n”,count[n-1]); return 0; } 最短距離 経路の数
  120. おしまい ありがとうございした おつかれさました

  121. おまけ 以下は作ったけど使わなかった かわいそうなスライドたちです

  122. 遊び3 以下のようなグラフを全部書き出す ・頂点が6個(頂点どうしは区別しない) ・辺が5本 ・全部の頂点が繋がっている 繋がっていないのでダメ 全部で6通り

  123. 遊び3.1 さっき作ったグラフを1つ選んで、 好きなところに1本辺を追加する

  124. 例 必ず 輪っか が1つできる ループ または 閉路 と呼ぶ

  125. 木とは ループがない連結なグラフ 全部が繋がっているということ

  126. 木の性質 辺の数 = 頂点の数 - 1 ある頂点からある頂点まで、同じ辺を2回以上通 らずに行く方法は1通りのみ

  127. 根付き木 根のある木(根は1つ) 木の有向バージョン 根 葉 葉 葉 葉 葉 葉

    節点 節点 節点 節点 枝