ACPC freshmen training
http://w.atwiki.jp/icpctrain/
ACPC freshmen training
ja
2013-11-16T21:43:52+09:00
1384605832
-
AOJ 1132 Circle and Points
https://w.atwiki.jp/icpctrain/pages/43.html
*AOJ1132 Circle and Points
**解説
***解法1
任意の二点を選んでその点を通る円を作る。円は2個ある。
そのような全ての円それぞれが包含できる点の数を数えて最大値を求める。N<=300なので間に合う。
円の中心点の求め方
選んだ2点a, bの作る直線Aの生む垂直二等分線と、2点のうち一方の点と中心を結ぶ長さ1の線分から、三平方の定理でAと中心の距離を求める。
後は、線分abの中点からaまたはbに向かうベクトルを90度回転してやれば、その位置が円の中心となる。
点を包含できるかの判定方法は、ベクトルを用いて円の中心を始点と考えた時の点までの長さと半径の比較で判定できる。
2013-11-16T21:43:52+09:00
1384605832
-
Geometry
https://w.atwiki.jp/icpctrain/pages/42.html
*Geometry
**Circle And Points
-[[AOJ 1132 Circle and Points]]
2013-11-16T21:41:22+09:00
1384605682
-
メニュー
https://w.atwiki.jp/icpctrain/pages/2.html
**メニュー
-[[トップページ]]
----
**STL
-[[ctype.h]]
-[[stdlib.h]]
-[[algorithm]]
-[[vector]]
----
**AOJ / UVa / Livearchive
-[[Dynamic programming]]
-[[String processing and parsing]]
-[[Graph]]
-[[Brute Force]]
-[[Backtracking]]
-[[Geometry]]
----
**Algorithm
-[[オイラー路]]
-[[繰り返し二乗法]]
----
**Tips
-[[ビット操作]]
-[[ある範囲の中から同じ種類の数を答える]]
----
**Contest
-[[Regular Contest]]
-[[DP Contest]]
-[[Parser Contest]]
----
//**更新履歴
//#recent(20)
&link_editmenu(text=ここを編集)
2013-11-16T21:39:20+09:00
1384605560
-
AOJ1155 How can I satisfy thee? Let me count the ways...
https://w.atwiki.jp/icpctrain/pages/33.html
*AOJ1155 How can I satisfy thee? Let me count the ways...
**サイト
[http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=1155&lang=jp]
**問題俯瞰
問題は与えられた式をある規則によって計算した結果が2となるときのP, Q, Rの組み合わせ数を答えるものです。
P, Q, Rは0, 1, 2とそれぞれ値が変化するので3重ループのfor文で回して27通り試しそのたびに構文解析をし直せばよいです。
Sample Inputが 2 であり、その結果 27 を返すものがあります。これはP, Q, Rがそれぞれいかなる値でも常に計算結果が2であるのでP, Q, Rの組み合わせ数は27通りということを示します。
**解説
以下の与えられたBNF((BNF = バッカス・ナウア記法(Backus-Naur form)といいます。2人の人の名前をつなげてます))に従って再帰的下向き構文解析のコードを書くのみです。
>||
<formula> ::= 0 | 1 | 2 | P | Q | R |
-<formula> | (<formula>*<formula>) | (<formula>+<formula>)
||<
電卓のときは<expression> <term> <factor>というように3つの要素がありましたが、今度は<formula>のみなので作る再帰関数もformula()のみ存在すれば必要十分です。
参考文献 → [https://gist.github.com/draftcode/1357281:title=構文解析 Howto]
さて、先に与えられた表の計算の仕組みを考えなければなりません。
XやY(この場合それぞれ<formula>)があるときに与えられている真理表において -X, (X*Y), (X+Y) はどのような仕組みで行われているでしょうか。
パターン数が少ないので演算の関係をすべて配列に埋め込んで条件分岐させて解いても良いですが、ちょっと格好良く(?)やりたいところです。
-X は 0が2, 1が1, 2が0 となります。if文を3つ書いても良いですが、「2からXを引く」とすれば
順に、2から0引いて2、2から1引いて1、2から2引いて0となり優雅に一行で済ませられます。
(X*Y)や(X+Y)はどうでしょう。
XとYの数の対応を見て
>>
(X*Y) 数の大きい方の数字を選んでいる=両者のmaxを取ればよい
(X+Y) 数の小さい方の数字を選んでいる=両者のminを取ればよい
<<
となると思います。
BNFのうち簡単なものから処理しましょう。構文解析する上で読み進めるポインタの挙動を考えるのはひとまず後回しにします。
0 | 1 | 2 | P | Q | R | の部分はそれぞれの文字を読み込んだときに具体的な値やP, Q, Rの変数の値を返せば良さそうです。
-Xについては上に示したとおりで、併せて以下のように書けます。
>||
int formula() {
...
switch( *p ) {
case '-': return 2 - formula();
case 'P': return P;
case 'Q': return Q;
case 'R': return R;
case '0': return 0;
case '1': return 1;
case '2': return 2;
...
}
||<
これでBNFの以下の部分全てが解析出来ました。(と思えればOKです)
>||
<formula> ::= 0 | 1 | 2 | P | Q | R |
-<formula>
||<
残すは括弧がある計算式です。計算方法は説明したとおりなのでポインタの挙動を考えなければ下のように書けます。
'('を読めたら、<formula>を読んで結果を保持し、 * or + で分岐してさらに<formula>を読み取り演算します。
>||
...( 0 | 1 | 2 | P | Q | R の部分)
case '(':
int form1 = formula();
int form2;
...
switch( *p ) {
case '*':
form2 = formula();
...
return min( form1, form2 );
case '+':
form2 = formula();
...
return max( form1, form2 );
}
}
...
||<
ここまで書ければ残すはポインタの挙動を考えるのみです。
(因みにstringの要素を参照できればよいだけなので、str[pos]としてもイテレータを用いても何でも良いです。好みの問題です)
[https://gist.github.com/draftcode/1357281:title=構文解析 Howto] では、文字を調べた直後にイテレータをインクリメントしていますが、上のコードでHow toのやり方で文字を調べた直後にポインタをインクリメントするように記述したら、
>||
case 'P':
p++;
return P;
||<
というように書く事になります。これでよいです。ただインクリメントの書き漏らしや書きすぎが怖いので私は以下のようにしましました。
>||
int formula() {
p++;
switch( *p ) {
...
case 'P': return P;
}
||<
先に位置をインクリメントしてその後にそこの位置にある文字を調べる、とするとp++を書く回数が少なくて済みます。
ただし、
前者の「文字を調べてからインクリメント」方式と
後者の「インクリメントしてから文字を調べる」方式では、解析する数式の先頭位置が異なります。
前者は p = str.c_str(); として先頭からはじめて良いですが、後者は p = str.c_str() - 1; として先頭アドレスの一つ前を指定しなければならないので注意が必要です。自分がバグを生まないと思える書き方をするとよいです
====
補足:
stringのc_str()メソッドはstring型をconst char* として出力します。const char* とは読み取り用の文字列を明示したchar*です。
====
構文解析のように再帰的手続きを記述する場合は、具体的に考えすぎず目的の動作を抽象化してまとめてみると良いです。
解析位置を動かす基準はこうである、といえればバグを埋め込みにくくなります。
あとは終わり括弧の読み飛ばしに注意してコードを書いてみましょう。以下がその例です。
>||
#include <iostream>
#include <algorithm>
using namespace std;
int P, Q, R;
const char *p;
int formula() {
p++;
switch( *p ) {
case '-': return 2 - formula();
case 'P': return P;
case 'Q': return Q;
case 'R': return R;
case '0': return 0;
case '1': return 1;
case '2': return 2;
case '(':
int form1 = formula();
int form2;
p++;
switch( *p ) {
case '*':
form2 = formula();
p ++;
return min( form1, form2 );
case '+':
form2 = formula();
p++;
return max( form1, form2 );
}
}
return 0;
}
int main() {
string str;
while(getline(cin, str)) {
int cnt = 0;
if( str == "." ) break;
for(int i=0; i<3; i++) {
for(int j=0; j<3; j++) {
for(int k=0; k<3; k++) {
P = i, Q = j, R = k;
p = str.c_str()-1;
if( formula() == 2 ) cnt ++;
}
}
}
cout << cnt << endl;
}
return 0;
}
||<
2013-11-13T17:09:47+09:00
1384330187
-
vector
https://w.atwiki.jp/icpctrain/pages/41.html
*vector
**swap
vectorの中身を全て交換する
>||
void swap( vector &from );
Ex.
vecA.swap(vecB);
||<
2013-11-10T13:44:14+09:00
1384058654
-
algorithm
https://w.atwiki.jp/icpctrain/pages/39.html
*algorithm
**next_permutation
stringや他のコンテナの全ての順列を生成する。
>||
#include <algorithm>
...
string str = "ABCDEF";
do {
cout << str << endl;
} while(next_permutation(str.begin(), str.end()));
Result:
ABCDEF
ABCDFE
ABCEDF
ABCEFD
...
FEDCBA (計 6! = 720個の文字列を生成)
||<
2013-11-02T03:42:52+09:00
1383331372
-
繰り返し二乗法
https://w.atwiki.jp/icpctrain/pages/38.html
*繰り返し二乗法
**解説
0を含むすべて自然数は2のべき乗の和の組み合わせで表すことが出来る。2進数がその例である。
nを2のべき乗の和で表すと
>||
n = 2^(k_1) + 2^(k_2) + 2^(k_3) + ...
||<
よって、
>||
x^n = x^(2^k_1) x^(2^k_2) x^(2^k_3) ...
||<
2進数で操作をすればよいので1のビットが立つ部分iについて、resにx^iを掛けていく。
下位ビットから考えているのでxは順次二乗していけばよい。nの1と0のビットの数だけ処理するのみなのでO(log n)でxのべき乗を高速に求められる。
>||
typedef long long ll;
ll mod_pow(ll x, ll n, ll mod) {
ll res = 1;
while(n > 0) {
if(n & 1) res = res * x % mod;
x = x * x % mod;
n >>= 1;
}
return res;
}
||<
2013-11-02T03:10:04+09:00
1383329404
-
RMQ
https://w.atwiki.jp/icpctrain/pages/37.html
*RMQ ( RangeMinumumQuery )
**
2013-11-01T23:11:42+09:00
1383315102
-
AOJ0202 At Boss's Expense
https://w.atwiki.jp/icpctrain/pages/36.html
*AOJ0202 At Boss's Expense
**サイト
[http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0202]
**解説
上限金額が決まっていて、与えられた料理の金額を使ってそれぞれ0個以上用いて割り切れない最大の金額を求める。
「割り切れない」はすなわちその値が素数であることを示す。
「割り切れない」の意味が分かったので、取り敢えず「割り切れない」は横において、今度は問題の残りの部分の解釈に移る。
それぞれの料理を使う個数を決めて上限の金額を超えるまで全探索することはとてもできない。
添字が上限金額まで記憶可能なbool型の配列を用意して可能な合計金額をマーキングをしていこう。ある料理の値段を用いて新しくマークを付けるとき、既知のマークを始点としてある料理の金額分先にマーキングできる。初期条件としては合計金額が0円である自明なマーキングがある。
あとははじめの「割り切れない=素数である」をと合わせてマーキングされた中で答えになりうる最大の金額を見つければ良い。
>||
c[0] = 1;
for(int i=0; i<n; i++) {
cin >> b;
for(int j=b; j<MAX; j++) {
c[j] = c[j] | c[j-b];
}
}
int mx = -9999;
for(int i=0; i<=a; i++) {
if(c[i] && is_prime[i]) mx = max(mx, i);
}
if(mx == -9999) cout << "NA" << endl;
else cout << mx << endl;
||<
2013-11-01T22:56:55+09:00
1383314215
-
AOJ0557 A First Grader
https://w.atwiki.jp/icpctrain/pages/35.html
*AOJ0557 A First Grader
**サイト
[http://judge.u-aizu.ac.jp/onlinejudge/description.jsp?id=0557]
参考サイト
[http://d.hatena.ne.jp/Respect2D/20110212/1297501157]
[https://speakerdeck.com/kagamiz/aoj-0557-a-first-grader-jie-shuo]
**解説
+と-の2択を選びつつ様々な制限の元ansに一致する場合数を列挙します。
2択で処理を進めるので全探索するとO(2^n)です。取りあえず再帰で全探索コードを考えてみましょう。
数列の今見ている項とそこまでの計算結果の2つの状態が必要です。
終了条件でansと計算結果が一致していれば場合の数が1つ生じます。以下のコード上では例外処理が省略されています。
>||
int rec(int index, int sum) {
if(index == tail) return sum == ans;
return rec(index+1, sum + seq[index+1]) + rec(index+1, sum - seq[index+1]);
}
||<
同じ項を見ているときそこまでの計算結果も同じなら、その後の計算にダブりが生じます。何度も同じ計算をしないよう、計算結果の再利用をしましょう。メモ化再帰です。
>||
int rec(int index, int sum) {
if(memo[index][sum] != -1) return memo[index][sum];
if(index == tail) return sum == ans;
return memo[index][sum] = rec(index+1, sum + seq[index+1]) + rec(index+1, sum - seq[index+1]);
}
||<
これで無駄な計算が無くなり、O(ns) 100*21 = 2100通りとなりました。
再帰を考えることでindexやsumという状態を持つことが分かったので今度はDPしてみましょう。高校数学の漸化式のと同じようにDPには「初期条件」と「式そのもの」が必要です。
DPの式は以下です。
>>
dp[今見ている数列のi番目][計算結果] := 場合の数
<<
漸化式について帰納的に遷移を考えましょう。「計算結果」は「i-1番目の計算結果にseq[i]を足したものか、seq[i]を引いたもの」です。場合の数はその状態に辿り着くまでの経路数の和です。
初期条件を考えましょう。漸化式の計算が起こる前に予め場合の数が決まる状態が1つ存在します。場合の数は各項で+か-の二択を選択することで増加していきますが、数列の先頭ではその選択が起こりません。(「必ず+が選択される」とも言い換えられます)すなわち dp[0][seq[0]] = 1 を必ず言うことができるので、これが初期条件となります。
解答コード
>||
#include <iostream>
using namespace std;
long long dp[101][21]; // 0で初期化済み
int main() {
int n, ans;
int seq[100];
cin >> n;
for(int i=0; i<n-1; i++) {
cin >> seq[i];
}
cin >> ans;
dp[0][seq[0]] = 1;
for(int i=1; i<n-1; i++) {
for(int j=0; j<=20; j++) {
if(j+seq[i]<=20) dp[i][j+seq[i]] += dp[i-1][j];
if(j-seq[i]>=0) dp[i][j-seq[i]] += dp[i-1][j];
}
}
cout << dp[n-2][ans] << endl;
return 0;
}
||<
2013-11-01T22:44:42+09:00
1383313482