31
1 ACM-ICPC2010 国内予選問題の解説 ACM-ICPC2010 の国内予選問題は http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems 同問題の判定データは http://icpc2010.honiden.nii.ac.jp/files/qualify_judge/qualify_judge.zip 本日の資料は http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2010/solution.zip ちなみに ACM-ICPC2009 の国内予選問題に関する昨年行った解説の資料は http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2009/solution.zip にあります。 今年の国内予選参加のために必要な準備等は、安田先生が http://ylb.jp/2011a/procon/ にまとめて下さっていますので、良く読んで準備をしてください。

ACM-ICPC2010 国内予選問題の解説ex.ylb.jp/icpc/ICPC2010DomPreSol.pdf · // ACM -ICPC 2010 Japan Online Contest Problem B // // ファイル名: b1.c // コンパイル方法:

  • Upload
    others

  • View
    4

  • Download
    0

Embed Size (px)

Citation preview

  • 1

    ACM-ICPC2010 国内予選問題の解説 ACM-ICPC2010 の国内予選問題は http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems 同問題の判定データは http://icpc2010.honiden.nii.ac.jp/files/qualify_judge/qualify_judge.zip 本日の資料は http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2010/solution.zip ちなみに ACM-ICPC2009 の国内予選問題に関する昨年行った解説の資料は http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2009/solution.zip にあります。 今年の国内予選参加のために必要な準備等は、安田先生が http://ylb.jp/2011a/procon/ にまとめて下さっていますので、良く読んで準備をしてください。

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems�http://icpc2010.honiden.nii.ac.jp/files/qualify_judge/qualify_judge.zip�http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2010/solution.zip�http://www.cse.kyoto-su.ac.jp/~hiraishi/ICPC/2009/solution.zip�http://ylb.jp/2011a/procon/�

  • 2

    [Problem A] 角角画伯,かく悩みき シミュレーション問題。入力データ ”ni di” は、i 番目の正方形を ni 番の正方形に対してdi で示される方角に隣接して置くことを指示している。この指示をシミュレートするためには、各正方形の座標が必要となるので、i 番目の正方形の x 座標を格納する配列 x[ ]と y座標を格納する配列 y[ ]を用いる。 最初の正方形はどこに置いても良いので(0,0)に置くことにする。 di の方角の隣接位置(左側=0、下側=1、右側=2、上側=3)に置くには int dx[ ] = {-1, 0, 1, 0}; int dy[ ] = {0, -1, 0, 1}; としておいて、 x[i] = x[ni] + dx[di]; y[i] = y[ni] + dy[di]; とすれば switch 文などによる場合分けは不要となる。 正方形を一つ置くたびに、正方形の x 座標の最小値 xmin と最大値 xmax、y 座標の最小値ymin と最大値 ymax を更新しておく。 最終的に、求める幅は xmax – xmin + 1, 高さは ymax – ymin + 1 で与えられる。

    [Tips] 作成したプログラムの動作確認 「Sample Input」 に書かれた内容を持つファイルを A0, 「Output for the Sample Input」に書かれた内容を持つファイルを A0.ans とする。作成したプログラムを a.out とするとき ./a.out < A0 > A0.result

    のように実行し diff A0.ans A0.result

    で何も出力されなければ、A0.ans と A0.result の内容が一致している (即ちプログラムは正しい答えを出力している)。

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_A�

  • 3

    プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem A // // ファイル名: a.c // コンパイル方法: cc a.c // 実行方法: ./a.out < A0 > A0.result など // チェック方法: diff A0.ans A0.result など // // アルゴリズム概要 // 最初の正方形は(0,0)に置くものとする。 // 2 枚目以降の正方形の置く位置は入力データより求める。 // 各正方形の座標は、配列 x[],y[]に記憶させる。 // x 座標の最小値、最大値、y 座標の最小値、最大値を求める。 // 幅は、x 座標の最大値 - x 座標の最小値 + 1, // 高さは、y 座標の最大値 - y 座標の最小値 + 1 である。 #include #define MAXN 200 // 1 ymax) ymax = y[i]; } // 幅と高さを出力 printf("%d %d¥n", xmax-xmin+1, ymax-ymin+1); } }

  • 4

    [Problem B] 迷路と命ず 迷路に対する最短経路長を求める問題。迷路を表すマップ上で幅優先

    探索を行えばよい。例えば右図のような迷路では 1. 入口の部分に「1」のラベルをふる 2. マップ上で「1」のラベルが付いた部分に対して、その隣接位置

    へ移動可能(間に壁が無い)で、かつまだラベルが付いていない部分に「2」のラベルをふる

    3. マップ上で「2」のラベルが付いた部分に対して、上記と同様の処理を繰り返し「3」のラベルをふる

    4. このような操作を出口にラベルがふられるか、あるいは、新しいラベル付けが行われなくなるまで繰り返す

    5. もし出口にラベルが付いていれば、その値が最短経路長である。出口にラベルが付いていなければ、入口から出口に至る経路が無いことを意味している。

    この問題の壁のデータの与え方が変わっている。その意図をくみ取ってみよう。 壁のデータを文字の 2 次元配列で読み込んだとする。

    これより、迷路のマップで(a,b)と(c,d)が隣接しているときに、その間に壁があるかどうかは、壁データの方で a+c 行 b+d 列の値を見ることで分かる。(値が1なら壁あり、値が 0なら壁無) 空白を含む文字列を入力するには、gets 関数を用いれば良いが、gets 関数を使用したプログラムを実行すると「warning: this program uses gets(), which is unsafe」といったバッファーオーバーフローに関する警告がでる環境が多くなっている。そのため、gets の代わりに fgets 関数を用いて標準入力ファイルである stdin から文字列を入力するようにすれば良い。 1. scanf で文字列を入力する場合、空白文字を入力することはできない。 scanf と fgets の両者を用いる場合注意が必要である。この問題の場合、最初の行の入力である整数 w と h を scanf で読みこみ、それに続く 2×h-1 行を文字列として fgets で読

    0 1 2 0 1 1 0 1 2 0 3 1 0 4 1

    0 1

    0

    1

    2

    壁データ 迷路のマップ 0 行 1 列の 1 (0,0)-(0,1)の間の壁 1 行 0 列の 0 (0,0)-(1,0)の間は壁無 1 行 2 列の 1 (0,1)-(1,1)の間の壁 2 行 1 列の 0 (1,0)-(1,1)の間は壁無 3 行 0 列の 1 (1,0)-(2,0)の間の壁 3 行 2 列の 0 (1,1)-(2,1)の間は壁無 4 行 1 列の 1 (2,0)-(2,1)の間の壁

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_B�

  • 5

    み込むとすると、scanf の後で 1 回 getchar( )を行う必要がある(プログラム例参照)。これを行わないと、scanf 直後の fgets では空行(改行のみの行)が読み込まれてしまう。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem B // // ファイル名: b1.c // コンパイル方法: cc b1.c // 実行方法: ./a.out < B0 > B0.result など // チェック方法: diff B0.ans B0.result など // // アルゴリズム概要 // キューを用いない簡易版の幅優先探索 (n = w * h とすると O(n*n)) // 壁のデータは文字列のまま配列 wall に格納 // (x,y)と(x+1,y)の間に壁有 wall[2*y][2*x+1]=='1' // (x,y)と(x-1,y)の間に壁有 wall[2*y][2*x-1]=='1' // (x,y)と(x,y+1)の間に壁有 wall[2*y+1][2*x] == '1' // (x,y)と(x,y-1)の間に壁有 wall[2*y-1][2*x] == '1' // (x,y)の隣接位置を(nx,ny)とすると // (x,y)と(nx,ny)の間に壁有 wall[y+ny][x+nx] == '1' #include #define MAXWH 30 // 幅、高さの最大値 int map[MAXWH][MAXWH]; // 経路探索用の地図。入口からの最短距離を格納 char wall[2*MAXWH-1][2*MAXWH+2]; // 壁のデータ // 隣接位置のオフセット 右,上,左,下 int dx[4] = {1, 0, -1, 0}; int dy[4] = {0, -1, 0, 1}; int w, h; // 迷路の幅と高さ bfs() // 簡易版の幅優先探索 (n=w*h とすると O(n*n)) { int x,y, len,flag,d; int nx, ny; for(y=0; y

  • 6

    if(wall[y+ny][x+nx]=='1') continue; // 進行方向は壁なのでスキップ map[ny][nx] = len+1; // 新しい場所に来たので、最短距離(len+1)を格納 if(ny==h-1 && nx==w-1){ // 迷路の出口なので printf("%d¥n",len+1); // 求まった最短距離をプリント return; } flag = 1; // 新たに最短距離が求まった場所がある。 } } } if(flag==0) break; // 新たに最短距離が求まった場所がなければ探索終了 } printf("0¥n"); // 出口に到達できなかった。 } main() { int i; while(1){ scanf("%d %d", &w, &h); // 幅と高さの入力 if(w==0 && h==0) break; // 両方共 0 なら終了 getchar(); // w と h の行の行末の改行コードを読み飛ばす for(i=0; i< 2*h-1; i++){ // 壁データの読み込み fgets(wall[i],2*MAXWH+1,stdin); // 文字列として読み込む } bfs(); // 幅優先探索により最短距離を求める。 } }

  • 7

    [Problem C] ボロック予想 動的計画法(Dynamic Programming, DP)により解ける。 x を表現するのに必要な正四面体数の個数の最小値を n_tetra[x]とする。 正四面体数 1, 4, 10, 20, 35, … , x 以下の最大の正四面体数 に対して、n_tetra[x]は、{n_tetra[x – 1], n_tetra[x – 4], n_tetra[x – 10], n_tetra[x – 20], n_tetra[x – 35], …, n_tetra[x – x 以下の最大の正四面体数]}の最小値+1 で求めることができる。ここで、n_tetra[0] = 0 である。したがって、x = 1 から順に x を一つずつ増やしながら n_tetra[x]を求めていくことができる。 問題として与えられる x の最大値は 1,000,000 であり、上記のアルゴリズムでは、一度n_tetra[x]を計算すると、1 ≦ y < x に対して n_tetra[y]も求まっている。したがって最初に準備として百万未満の全ての整数 x に対して n_tetra[x]を求めておけば、与えられた数値に対する答えは n_tetra[与えられた数値]を見るだけで得られる。 x を表現するのに必要な奇数の正四面体数の個数の最小値も同様の考え方で求められる。すなわち、求める値を n_odd_t[x]とすると、奇数の正四面体数 1, 35, …, x 以下の奇数の正四面体数に対して、n_odd_t[x] は{n_odd_t[x – 1], n_odd_t[x – 35], …, n_odd_t[x – x 以下の奇数の正四面体数]}の最小値+1で求めることができる。 整数 a が奇数かどうかの判定は、a % 2 あるいは a & 1 が1となるかどうかで判定できる。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem C // // ファイル名: c1.c // コンパイル方法: cc c1.c // 実行方法: ./a.out < C0 > C0.result など // チェック方法: diff C0.ans C0.result など // // アルゴリズム概要 // x を表すために必要な正四面体数の個数の最小値を n_tetra[x]とすると // 正四面体数 1, 4, 10, ... に対して、 // n_tetra[x] = min(n_tetra[x-1], n_tetra[x-4], n_tetra[x-10], ...) + 1 // となる。即ち n_tetra[x] の値は y

  • 8

    #define N 1000000 // 与えられるデータは 1000000 より小さい int n_tetra[N]; // 正四面体数の最小個数を入れる配列 int n_odd_t[N]; // 奇数の正四面体数の最小個数を入れる配列 // 1 n_tetra[x - tetra] + 1) n_tetra[x] = n_tetra[x - tetra] + 1; if(tetra & 1){ // 正四面体数が奇数の場合も同様にチェック if(n_odd_t[x] > n_odd_t[x-tetra]+1) n_odd_t[x] = n_odd_t[x-tetra]+1; } } } } main() { int x; // まず最初に 1

  • 9

    [Problem D] ぐらぐら 一般に、n 個の物体があり、i 番目の物体の重心の x 座標を xi, 重さを wi とすると、全体の

    重心の x 座標と重さ w は ∑∑==

    =⋅=n

    ii

    n

    iii wwwwxx

    11,/)( となる。

    良さそうな方法は思いつかなかった。アイデア募集中!!! ので、少し強引に解いている。 入力データの読み込みは、w と h を scanf で読み込み、getchar でその行の行末コードを読み込み、ピースのデータは fgets で char 型の2次元配列に読み込んでいる。 不要かもしれないが、各ピースにユニークなピース番号をふっておいた方が後の処理での

    混乱が少なくなると思われるので、別途整数型の2次元配列を用意し、そこにユニークな

    ピース番号をふっている。 各ピースごとに、 1) そのピースを構成する4つのブロックの座標 2) 自分の下側に接しているピースの番号 3) 下側に接している面の最左 x 座標と最右 x 座標 4) 上側に接しているピース(本ピースが直接支えているピース)の個数(最大で4) 5) 上側に接しているピースの番号 6) 自分自身を含めて支えているピース全体の重量と重心の x 座標 を求めている。 各ピースの支えあう状態は、地面に接しているピースを根とする木構造で表現できる(と

    問題に書かれている)ので、各ピースに対して 1)~5)を求めることにより木構造を表現する。次に、地面に接しているピース(木構造の根)から post order の深さ優先探索で 6)を求めていく。各ピースにおいて 6)が求まった時点で、重心の x 座標が 3)で求めた最左 x 座標と最右 x 座標の間に入っているかをチェックし、入っていなければ「UNSTABLE」と判定する。途中で「UNSTABLE」と判定されることなく根節点に辿り着き、根節点も「UNSTABLE」でなければ「STABLE」と判定する。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem D // // ファイル名: d.c // コンパイル方法: cc d.c // 実行方法: ./a.out < D0 > D0.result など // チェック方法: diff D0.ans D0.result など

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_D�

  • 10

    // // アルゴリズム概要 // 1. 各ブロックにユニークなピース番号をふる // 2. 各ピースに属しているブロックとそれらに隣接しているブロックを調べ // 下側のピース番号と下側ピースに接している最左、最右 x 座標 // 上側のピース(本ピースが支えているピース)の個数とそれらのピース番号 // を求める // 3. 地面に接しているピースから post order で、各ピースが支える全重量、重心位置を // を求め、安定性をチェックする。不安定なものが見つかれば「不安定」、 // 全てが安定なら「安定」と判定 #include #define WMAX 10 // 幅 w の最大値 #define HMAX 60 // 高さ h の最大値 #define PMAX (WMAX*HMAX/4) // ピースの個数の最大値 struct piece_t { int xpos[4]; // ピースを構成するブロックの x 座標 int ypos[4]; // ピースを構成するブロックの x 座標 int down; // 下側のピース番号 int xl; // 下側ピースに接している面の最左 x 座標 int xr; // 下側ピースに接している面の最右 x 座標 int nup; // 上側のピース(本ピースが支えているピース)の個数 int up[4]; // 上側のピース(本ピースが支えているピース)番号 int weight; // 自分自身を含め支えているピース全体の重量 double xcg; // 自分自身を含め支えているピース全体の重心の x 座標 }; int w,h; char map[HMAX][WMAX+2]; // 入力データを文字列で記憶する配列 int pnum[HMAX][WMAX]; // ピース番号を覚える配列 struct piece_t pdata[PMAX]; // 各ピースのデータを格納する配列 int np; // ピースの個数 // 隣接位置のオフセット(右、上、左、下) int dx[] = {1, 0, -1, 0}; int dy[] = {0, 1, 0, -1}; // 深さ優先探索により安定性をチェックし、安定なら0、不安定なら1を返す int dfs(int pn) { int i; int tw; double cg; tw = 0; cg = 0; for(i=0; i< pdata[pn-1].nup; i++){ // 自分が支えている各ピースに対し if(dfs(pdata[pn-1].up[i])) return 1; // それが不安定なら「不安定」と判定 // 安定な場合は、その重みと重心計算のためのモーメントを足し込む tw += pdata[pdata[pn-1].up[i]-1].weight; cg += pdata[pdata[pn-1].up[i]-1].weight * pdata[pdata[pn-1].up[i]-1].xcg; } for(i=0; i

  • 11

    pdata[pn-1].xcg = cg/tw; // このピースが支えている重みの重心の x 座標 // 重心位置が下側面の最左位置以下あるいは最右位置以上ならば不安定 if(pdata[pn-1].xcg = pdata[pn-1].xr){ return 1; // 不安定 } return 0; // 安定 } // ピースに関する情報を作成 gen_pdata() { int i,j,k; int x, y; for(i=0; i pdata[i].xr) pdata[i].xr = x+1; // より右にあれば最右 x 座標を更新 pdata[i].down = pnum[y-1][x]; // 下にあるピースの番号をセット } } else { // y == 0 すなわち地面に接している if(x < pdata[i].xl) pdata[i].xl = x; // より左にあれば最左 x 座標を更新 if(x+1 > pdata[i].xr) pdata[i].xr = x+1; // より右にあれば最右 x 座標を更新 pdata[i].down = 0; // 下にあるピースの番号として0(地面)をセット } if(y < h-1){ // 最も上の場所ではない(この上に他のピースがある可能性有り) if(pnum[y+1][x] >= 1 && pnum[y+1][x] != i+1){ // 自分と異なるピースのブロックが上にあるケース int match = 0; for(k=0; k

  • 12

    int x, y; char myx; int qf, qr; np = 0; // ピースの個数を0に初期化 for(y=0; y

  • 13

    } } [Problem E] 最強の呪文

    例えば、上図のような場合を考えると、節点 0(スター)から節点 1 に至るパスの最強の呪文は「aa」であるが、節点 0 から節点 2 に至るパスの最強の呪文は「aabc」であり、節点0 と節点 1 の間のパスとして最強の「aa」は用いられていない。したがって、スターから各節点への最強の呪文を求めていく方法は旨く機能しないと考えられる。

    一方、上図において、節点 2 から節点 3(ゴールド)に至るパスの最強の呪文は「cde」であり、節点 1 から節点 2 を経由して節点 3 に至るパスの最強呪文は「aacde」であり、節点2 と節点 3 の間のパスとして最強の「cde」が用いられている。したがって、各節点からゴールドに至るパスの最強呪文をパスを構成する枝数の順に求めていけば、スターからゴー

    ルドにいたるパスの最強呪文を求めることができる(存在する場合)。

    最強の呪文が存在する場合、その呪文にはループは含まれない。もし上図のようなループ

    を含んでいるとすると、「αβγ」の方が「αγ」より強い事を意味し、その場合、「αβ

    βγ」は「αβγ」より強くなる。したがってループを回る回数を増やすことによりいく

    らでも強い呪文を作ることができ、最強の呪文は存在しなくなる。 これらの考察より、節点数を n とするときに、枝の本数を 1 から n – 1 まで順次増やしながら、指定の枝の本数以下のパスを対象として、各節点からゴールドにいたるパスの中で

    の最強の呪文を求めていく。これにより最強の呪文が存在する場合は、ループ無しの最強

    呪文が「スター」節点のところに求まっている。プログラム例では、この操作を1サイク

    ルと呼んでいる。

    01 2

    α

    β

    γスター

    ゴールド

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_E�

  • 14

    最強の呪文が存在するかどうかについては、もう少し検討が必要である。各枝のラベルの

    文字数は1~6なので、同じ本数の枝を持つパスで生成される呪文の長さは最大で6倍の

    差がある。

    例えば、上図のようなケースを考える。1サイクル終了した時点では、スター節点のとこ

    ろに最強呪文として「aaaaaac」が求まる。しかしながら、サイクルを繰り返していくと、やがてスター節点のところに「aaaaaac」より強い呪文として「aaaaaab」が現れるので、最強呪文は存在しないと判定できる。枝のラベルの文字数は最大で6倍の差があるので、

    おそらく合計6サイクルぐらい計算を行えば、最強呪文が存在しない場合には、1サイク

    ル終了時点での最強呪文よりも強いものが現れると思われる(本当?)。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem E // // ファイル名: e.c // コンパイル方法: cc e.c // 実行方法: ./a.out < E0 > E0.result など // チェック方法: diff E0.ans E0.result など // // アルゴリズム概略 // 各節点に対して、その節点から枝 k 本以内でゴールドに至る最強呪文を // k = 1, ... , n-1 の順に求める。この操作を1サイクルと名付ける。 // 最強呪文が存在する場合、それはループを含まないので、 // スター節点の所には、その時点で最強呪文が求まっている。 // これが最強呪文であるためには、ループによりこれより強い最強呪文を // 任意個作りだすことができないことを確認する必要がある。 // スター節点の所に求まっている最強呪文の長さは高々6(n-1)であり、 // より強い最強呪文を作り出すループが存在する場合、そのループに含まれる // 文字列長は最短の場合 1 である。従って、合計で 6 サイクル分(?)計算を行えば、 // 1 サイクル目に求められたスター節点の最強呪文よりも強い呪文が求まるはずである。 // 既に 1 サイクル目の計算は終わっているので、あと 5 サイクル分の計算を行ってみて、 // より強い呪文が出てくれば、最強呪文は存在しない。 // 一方、より強い呪文が出てこなければ、1 サイクル目に求まっていたものが // 最強呪文となる。 #include #include #include #define MAX_N 40 // 節点数の最大値 #define MAX_A 400 // 枝数の最大値 #define MAX_LABEL_LEN 6 // 枝のラベルの最大文字列長 int n; // 節点数 2

  • 15

    int a; // 枝数 0

  • 16

    else { // 始点節点に対し、新呪文が登録済 // 新しい呪文が既登録の新呪文よりも強ければ if(strcmp(nl, node[sn].new) < 0){ free(node[sn].new); // 古い新呪文の領域を解放し node[sn].new = nl; // 新たに見つかった呪文を新呪文として登録 flag = 1; // 変化があったことを示す flag をセット } } } if(flag){ // より強力な新呪文が見つかっていれば for(j=0; j

  • 17

    for(i=0; i

  • 18

    [Problem F] 古い記憶 良さそうな方法は思いつかなかった。アイディア募集中!!! かなり強引に解いている。各ジャッジデータに対して、30秒程度かかる。 元の文章と改ざん文章の関係を考える。ウィルス感染の可能性は高々2回であり、各々の

    感染では、1文字削除、1文字追加、1文字変更が行われる。削除と追加は双対な関係で

    あるので、改ざん文章に対して、 改ざん文章そのもの(実際にはウィルス感染が起こっていなかった) 改ざん文章から任意の1文字を削除(元の文章に対しては1文字追加が発生) 改ざん文章の任意の1文字を変更(元の文章に対して1文字変更が発生) 改ざん文章の任意の場所に任意の文字を追加(元の文章に対しては1文字削除が発生) 改ざん文章に対して任意の2文字を削除 改ざん文章に対して任意の2文字を変更 改ざん文章に対して任意の2文字を任意の位置に追加 改ざん文章に対して1文字削除と1文字追加 改ざん文章に対して1文字削除と1文字変更 改ざん文章に対して1文字変更と1文字追加 により元の文章の候補を生成する。候補の生成において、文字の変更や追加に対しては、

    該当部分の文字を「?」としておく。 このような「?」を含む文章に対して、文章の各位置が少なくとも1個のピースでカバーされることをチェックする。 「?」を一つも含まない場合は、候補文章に対して、各ピースが部分文字列として現れるかをチェックし、候補文章の全ての位置が少なくとも一つ以上のピースの部分文字列となっ

    ていれば候補として採用する。 「?」を含む場合は、「?」ごとに、まず「?」に代入することによりその部分があるピースの部分文字列となる可能性がある文字のリストを求める。その後「?」にそれらの文字を順次代入して、「?」を含まない場合と同様の判定を行う。 なお、同じ候補文字列を複数回チェックするのをさけるため、チェックした文字列は2分

    探索木に登録しておく。また、最終的に候補となる文章の個数が5以下の場合は、その文

    章を辞書式順にプリントする必要があるので、候補となることが分かった文章も2分探索

    木に登録している。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem F // // ファイル名: f.c // コンパイル方法: cc f.c // 実行方法: ./a.out < F0 > F0.result など

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_F�

  • 19

    // チェック方法: diff F0.ans F0.result など // // アルゴリズム概略 // 改ざん文章に最大 2 回の文字削除/変更/挿入を行ったものが候補文章となりうる // 変更や挿入に対し、その位置の文字を'?'で表し、これらの操作により得られる // 文字列を作成。各ピースに対して、そのピースが'?'部分をカーバーできる可能性を // 探り、可能性がある場合にその'?'が取りうる文字のリストを作成する。 // '?'位置に候補となりうる文字を代入した文字列を作成し、その文字列の各文字が // 少なくとも一つのピースによりカバーされているかをチェックする。 // なお文字の削除しか行わない場合や、そもそも感染していない場合は、チェック対象の // 文章に'?'は含まれないので、この文章の各文字が少なくとも一つのピースにより // カバーされているかどうかを判定すればよい。 // 同じ文字列を繰り返しチェックするのをさけるため、チェック済みの文字列は // 二分探索木で管理する。また、候補となる文字列も 2 分探索木で管理する。 // 後者の 2 分探索木を in order で出力すれば候補文字列を辞書順に出力することができる。 #include #include #include #define MAX_N 30 // ピースの数の最大値 #define MAX_SLEN 42 // 元の文章の長さの最大値 = 改ざん文章の長さの最大値+2 #define MAX_PLEN 20 // ピースの長さの最大値 struct node_type { // 2 分探索木の節点の構造体 char *sentence; // 登録する文字列 struct node_type *left; // 左側の子供へのポインタ struct node_type *right; // 右側の子供へのポインタ }; typedef struct node_type node_t; // 上記の構造体の型を node_t と名付ける node_t *candidate; // 候補となる文章を格納するための 2 分探索木 node_t *checked; // チェックした文字列を格納するための 2 分探索木 char pieace[MAX_N][MAX_PLEN+1]; // ピース文字列を格納する配列 char original[MAX_SLEN+1]; // 元の文章を格納する配列 char altered[MAX_SLEN+1]; // 改ざん文章を格納する配列 char ans[5][MAX_SLEN+1]; // 可能な元の文章を格納する配列 int used[128]; // ピースの中に現れる文字を示す配列

    // used[c]==1 ---> 文字 c がピースに含まれる // used[c]==0 ---> 文字 c はピースに含まれない

    char chused[27]; // ピースに含まれている文字のリスト char chused1[27],chused2[27]; int d; // 感染回数の最大値 int n; // ピースの数 int nc; // ピースに現れる文字の種類数 int c; // もとの文章の候補数 int nc1, nc2; // 2 分探索木に文字列を登録する

  • 20

    // str = 登録する文字列, ppt = 2 分探索木の根節点へのポインタを格納している場所 // mode = 1 ---> 候補となる文字列の登録 // 戻り値: 0 ---> 新規登録成功 1 ---> 既に登録されていた int ins_bst(char *str, node_t **ppt, int mode) { node_t *pt; int sc; node_t *root; root = *ppt; // root -> 2 分探索木の根節点 if(root == NULL){ // 2 分探索木が空の場合 root = (node_t *)malloc(sizeof(node_t)); // 2 分探索木の節点を格納する領域を確保 root->left = root->right = NULL; // この新しい節点は左右の子を持たない root->sentence = (char *)malloc(strlen(str)+1); // この節点に文字列を格納する領域を確保 strcpy(root->sentence, str); // 確保した領域に文字列をコピー if(mode) c++; // 候補文字列を登録した場合は候補数を 1 増やす *ppt = root; // この節点を 2 分探索木の根節点とする return 0; // 新規登録成功 } pt = root; // pt -> 2 分探索木の根節点 while(pt){ // pt が NULL で無い間以下を繰り返す sc = strcmp(str, pt->sentence); // 今回登録する文字列と // pt が指す節点に登録されている文字列を比較 if(sc < 0){ // 今回登録する文字列の方が小さく if(pt->left) // 左の子が存在するなら pt = pt->left; // pt を左の子へのポインタとする else break; // 左の子が存在しない場合は、繰り返しを終了 } else if (sc == 0){ // 今回登録する文字列がこの節点に登録済なら return 1; // 1を返す(登録済) } else { // 今回登録する文字列の方が大きく if(pt->right) // 右側の子が存在するなら pt = pt->right; // pt を右の子へのポインタとする else break; // 右の子が存在しない場合は、繰り返しを終了 } } if(scleft = (node_t *)malloc(sizeof(node_t)); // 左の子を格納する領域を確保し pt = pt->left; // pt を左の子へのポインタとする } else { // 今回登録する文字列の方が大きく右の子が存在しない場合 pt->right = (node_t *)malloc(sizeof(node_t)); // 右の子を格納する領域を確保し pt = pt->right; // pt を右の子へのポインタとする } pt->left = pt->right = NULL; // pt が指す新しい節点は左右の子は持たない pt->sentence = (char *)malloc(strlen(str)+1); // その節点に登録する文字列を格納する // 領域を確保 strcpy(pt->sentence, str); // 確保した領域へ文字列をコピー if(mode) c++; // 候補文字列の登録なら、候補数を 1 増やす return 0; // 新規登録成功

  • 21

    } clean_bst(node_t *pt) // 2 分探索木が使用していた領域を解放 { if(pt){ free(pt->sentence); // pt が指す節点の文字列を格納していた領域を解放 clean_bst(pt->left); // 左の子以下を再帰的に post order 順に解放 clean_bst(pt->right); // 右の子以下を再帰的に post order 順に解放 free(pt); // 最後にこの節点自体の領域を解放(post order) } } // pt が指す節点を根節点とする 2 分探索木に登録されている文字列を // count 個辞書順(inorder)に印刷。戻り値は残り印刷個数。 int print_bst(node_t *pt, int count) { int pn; if(count==0) return 0; // count が0なら何もしない if(pt==NULL) return count; // pt が NULL なら count を返す count = print_bst(pt->left, count); // まず左の子以下を再帰的に in order で // count 個印刷 if(count == 0) return 0; // 残り印刷数が 0 なら終了 printf("%s¥n", pt->sentence); // この節点に登録されている文字列を印刷 count--; // count を 1 減らす count = print_bst(pt->right,count); // 右の子以下を再帰的に in order で // count 個印刷 return count; // 残り印刷数を返す } // 文字列 str が、元の文章の候補となりうるか判定し、 // 候補になる場合は、候補文字列を格納する二分探索木に登録し // 候補数を 1 増やす check0(char *str) { int cover[MAX_SLEN]; // 文字列 str の各文字がピース文字列により // カバーされているかを調べるための作業用配列 int i,j,len,lenp; char *cp, *pos; len = strlen(str); // 文字列 str の長さ for(i=0; i

  • 22

    } } for(i=0; i 最初の'?'の位置に文字 c が入りうる // used2[c]==1 ---> 二番目の'?'の位置に文字 c が入りうる int i,j,k; int lens, lenp; for(i=0; i

  • 23

    check(char *str) { char work[MAX_SLEN+1]; int i,j,len; int nq, q1, q2; int c1,c2; if(ins_bst(str, &checked,0)) return; // 既にこの文字列をチェック済みなら何もしない len = strlen(str); // len = 文字列の長さ nq = 0; // nq は '?'の個数 q1 = q2 = -1; // '?'の位置を見つける for(i=0; i

  • 24

    // level==1 ---> まず 1 文字削除した文章の可能性判定を行い、 // d==2 なら、更にもう 1 文字削除/変更/挿入した場合の可能性も判定 // level==2 ---> 1 文字削除した文章の可能性を判定 // (既に 1 文字削除した状態で呼び出される) check_del(char *str, int level) { int i,j,len; char *cp; char work[MAX_SLEN+1]; // 文字を削除するために使用する作業用配列 len = strlen(str); // len = 文字列の長さ for(i=0; i 1 文字変更した文章の可能性を判定 // (すでに 1 文字削除/変更した状態で呼び出される) check_chg(char *str, int level) { int i,j,k,len; char *cp, work[MAX_SLEN+1]; // 文字を変更するために使用する作業用配列 char cc; len = strlen(str); // len = 文字列の長さ strcpy(work,str); // まず作業用配列へ文字列をコピー for(i=0; i まず 1 文字挿入した文章の可能性判定を行い

  • 25

    // d==2 ならさらにもう 1 文字挿入した文章の可能性も判定 // level==2 ---> 1 文字挿入した文章の可能性を判定 // (既に 1 文字削除/変更/挿入した状態で呼び出される) check_ins(char *str, int level) { int i,j,k,len; char *cp; char work[MAX_SLEN+1]; // 文字を挿入するために使用する作業用配列 len = strlen(str); // len = 文字列の長さ for(i=0; i

  • 26

    { int i; while(1){ scanf("%d %d", &d, &n); // d と n の入力 if(d==0 && n==0) break; // 両方とも0なら終了 scanf("%s",altered); // 改ざん文章の入力 for(i=0; i

  • 27

    [Problem G] レーザー光の反射 この問題の場合、計算結果には誤差が含まれるので、結果の比較には誤差 0.001 以内で一致しているかを判定するプログラムを作成してある。 鏡の枚数は5枚以下で、反射回数も6回未満なので、6回未満の鏡の反射順各々に対して、

    実現可能性をチェックすると共に、実現可能なものの経路長の中から最短経路を求める。 0 回反射の場合 実現可能性: レーザー光源とターゲットを結ぶ線分が、どの鏡とも交わらない → 2線分の交差判定 経路の距離: レーザー光源とターゲットの距離 1 回反射の場合

    実現可能性: 光源の鏡による鏡像とターゲットを結ぶ線分が鏡と交差する。 交差点を c とする時、線分 sc と線分 ct は他の鏡と交わらない 経路の距離: 鏡像とターゲットの距離 2回反射の場合

    光源 s の鏡 m1 による鏡像を i1、鏡像 i1 の鏡 m2 による鏡像を i2 とする。

    http://icpc2010.honiden.nii.ac.jp/domestic-contest/problems#section_G�

  • 28

    実現可能性: ターゲット t と鏡像 i2 を結ぶ線分は鏡 m2 と交差する。 交差点を c2 とすると、c2 と鏡像 i1 を結ぶ線分は鏡 m1 と交差する。 その交差点を c1 とする。 線分 s – c1, c1 – c2, c2 – t は他の鏡と交差しない。 経路の距離: 線分 t – i2 の距離 3回以上反射する場合も同様に計算できる。 プログラム例 // ACM-ICPC 2010 Japan Online Contest Problem G // // ファイル名: g.c // コンパイル方法: cc g.c // 実行方法: ./a.out < G0 > G0.result など // チェック方法: ./compare // // アルゴリズム概略 // 鏡の枚数は 5 枚以下で、反射回数も 6 回未満なので、 // このようなケースを実現しうる鏡の反射順を全て生成し、 // その反射順が実現可能かどうかをチェックし、 // 実現可能なものについての経路長から最短経路長を求める。 // 選択された反射順に対して、まずレーザー発射位置の最初の鏡による鏡像を求め // 次に、その鏡像の 2 番目の鏡により鏡像を求める。これを繰り返し、 // 最後の鏡による鏡像の位置もとめる。この位置とターゲットの位置との距離が // 経路長となる(経路が実現可能な場合)。 // 実現可能性は、経路の反射位置が対応する鏡の上に存在し、間に経路を妨害する // 他の鏡が存在しないことを経路に沿ってチェックすることで判定できる。 // (本プログラムでは、このチェックを経路の逆順に沿って行っている。) #include #include struct point_type { // 点またはベクトルの x,y 座標を格納する構造体 double x; double y; }; typedef struct point_type point; // 点の x,y 座標 typedef struct point_type vector_t; // ベクトルの x,y 座標 struct line_type { // 線分の 2 点を格納する構造体 point p; point q; }; typedef struct line_type line; // 線(分)の 2 点 int n; // 1

  • 29

    point m_image[6]; // m_image[0] = laser, m_image[i] は i 番目の反射のイメージ double in_prod(vector_t a, vector_t b) // 内積を求める { return a.x*b.x + a.y*b.y; } double out_prod(vector_t a, vector_t b) // 外積を求める { return a.x*b.y - a.y*b.x; } double angle(vector_t a, vector_t b) // ベクトル a に対するベクトル b の角度を求める { return atan2(out_prod(a,b), in_prod(a,b)); } double distance(point a, point b) // 2 点間の距離を求める { return sqrt((a.x - b.x)*(a.x - b.x) + (a.y - b.y)*(a.y - b.y)); } point mirror_image(line m, point s) // 点 s の鏡 m に対する鏡像を求める { point image; double a_m, a_s, a_i, d; a_m = atan2(m.q.y - m.p.y, m.q.x - m.p.x); // x 軸に対する鏡の角度 a_s = atan2(s.y - m.p.y, s.x - m.p.x); // x 軸に対するベクトル ps の角度 a_i = a_m - (a_s - a_m); // x 軸に対するベクトル pi の角度 d = distance(m.p, s); image.x = d*cos(a_i)+m.p.x; image.y = d*sin(a_i)+m.p.y; return image; } // 線分 ab と線分 cd の交差判定。交差->1, 交差しない->0 int is_crossing(point a, point b, point c, point d) { double ta,tb,tc,td; tc = (a.x - b.x)*(c.y - a.y) + (a.y - b.y)*(a.x - c.x); td = (a.x - b.x)*(d.y - a.y) + (a.y - b.y)*(a.x - d.x); if(tc*td >= 0) return 0; ta = (c.x - d.x)*(a.y - c.y) + (c.y - d.y)*(c.x - a.x); tb = (c.x - d.x)*(b.y - c.y) + (c.y - d.y)*(c.x - b.x); if(ta*tb < 0) return 1; else return 0; } // 直線 ab と直線 cd の交点を求める。2直線は平行でないと仮定。 point cross_point(point a, point b, point c, point d) { double acx, acy, bunbo, r, s; point cross; acx = c.x - a.x; acy = c.y - a.y; bunbo = (b.x-a.x)*(d.y-c.y) - (b.y-a.y)*(d.x-c.x); r = ((d.y-c.y)*acx - (d.x-c.x)*acy)/bunbo;

  • 30

    s = ((b.y-a.y)*acx - (b.x-a.x)*acy)/bunbo; cross.x = a.x + r*(b.x - a.x); cross.y = a.y + r*(b.y - a.y); return cross; } // 与えられた鏡の反射回数に対して、可能性のある反射順を全て生成し、 // その反射順によりレーザー光がターゲットに到達できる場合は // その経路の距離を求め、既に求まっている経路長よりも短ければ // 経路長を更新する // level = 現時点での反射回数 nr = 今回チェックする反射回数 check(int level, int nr) { int i; if(level= length) return; // d が既に求まっている最短経路長以上ならばスキップ for(j=level; j>0; j--){ if(is_crossing(mirror[mno[j-1]].p, mirror[mno[j-1]].q, m_image[j], t)==0) break; // j 番目の鏡による反射が実現不可なら // チェック終了 // その鏡上の反射位置 cp を求める cp = cross_point(mirror[mno[j-1]].p, mirror[mno[j-1]].q, m_image[j], t); // cp とこの鏡の反射によるターゲット位置 t との間に他の鏡があれば実現不可 for(k=0; k

  • 31

    // 最初の鏡による反射の実現可能性をチェック for(k=0; k0){ // 鏡による反射が 1 回以上あるとき if(k==mno[0]) continue; // 最初の鏡は OK } // この時点で t は最初の鏡の反射位置 // レーザー発射位置と t の間に他の鏡があれば実現不可 if(is_crossing(mirror[k].p, mirror[k].q, laser, t)) break; } if(k==n) // 最後まで実現不可にならなかったので length = d; // 最短経路長を更新(この時点では必ず d < length である) } } solve() { // 座標値は 0~100 で鏡による反射回数は 6 回未満なので、 // 最短経路長は sqrt(2)*100*6 < 1000 以下である length = 1000; // とりあえず 1000 とし、より短い経路が見つかれば更新する m_image[0] = laser; // レーザー光発射位置をセット for(nr=0; nr /JPEG2000ColorImageDict > /AntiAliasGrayImages false /CropGrayImages true /GrayImageMinResolution 300 /GrayImageMinResolutionPolicy /OK /DownsampleGrayImages true /GrayImageDownsampleType /Bicubic /GrayImageResolution 300 /GrayImageDepth -1 /GrayImageMinDownsampleDepth 2 /GrayImageDownsampleThreshold 1.50000 /EncodeGrayImages true /GrayImageFilter /DCTEncode /AutoFilterGrayImages true /GrayImageAutoFilterStrategy /JPEG /GrayACSImageDict > /GrayImageDict > /JPEG2000GrayACSImageDict > /JPEG2000GrayImageDict > /AntiAliasMonoImages false /CropMonoImages true /MonoImageMinResolution 1200 /MonoImageMinResolutionPolicy /OK /DownsampleMonoImages true /MonoImageDownsampleType /Bicubic /MonoImageResolution 1200 /MonoImageDepth -1 /MonoImageDownsampleThreshold 1.50000 /EncodeMonoImages true /MonoImageFilter /CCITTFaxEncode /MonoImageDict > /AllowPSXObjects false /CheckCompliance [ /None ] /PDFX1aCheck false /PDFX3Check false /PDFXCompliantPDFOnly false /PDFXNoTrimBoxError true /PDFXTrimBoxToMediaBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXSetBleedBoxToMediaBox true /PDFXBleedBoxToTrimBoxOffset [ 0.00000 0.00000 0.00000 0.00000 ] /PDFXOutputIntentProfile () /PDFXOutputConditionIdentifier () /PDFXOutputCondition () /PDFXRegistryName () /PDFXTrapped /False

    /CreateJDFFile false /Description > /Namespace [ (Adobe) (Common) (1.0) ] /OtherNamespaces [ > /FormElements false /GenerateStructure false /IncludeBookmarks false /IncludeHyperlinks false /IncludeInteractive false /IncludeLayers false /IncludeProfiles false /MultimediaHandling /UseObjectSettings /Namespace [ (Adobe) (CreativeSuite) (2.0) ] /PDFXOutputIntentProfileSelector /DocumentCMYK /PreserveEditing true /UntaggedCMYKHandling /LeaveUntagged /UntaggedRGBHandling /UseDocumentProfile /UseDocumentBleed false >> ]>> setdistillerparams> setpagedevice