コンピュータ数学の即興問題(ク〇問注意報発令中)

1.1

リンゴが3個あります。1個50円です。全部で何円でしょうか。

 

1.2

リンゴが365個あります。2018年の元旦の時点では1個50円ですが, 日が変わるごとに1円ずつ値上がりします。2018年の元旦から大晦日まで毎日1個ずつリンゴを買うと、合計何円かかるでしょうか?

 

1.3

リンゴがおいしい季節です。今, 赤リンゴが1個100円で売られています。1人の子供は, 1000円のお金を持ってリンゴ(のみ)を買いに行き, 全部のお金を払いました。お店には, 赤リンゴのほかに青リンゴが売られていますが1個の値段はいくらだったか覚えていません。青リンゴを1個以上買ったこと・青リンゴが1円以上することが分かっているとき、青リンゴの値段[円]として考えられるものを安い順に重複を除いて列挙してください。

 

2.1

カレンダーは数学的に美しいです。1/1~12/31までを分数として扱ったとき、その和a/bはいくらになるでしょうか。aとbの値を求めてください。ただし、aとbの最大公約数は1とし, うるう年ではないとします。

 

3 (未解決).

10 * 10のマスに1 * 2のドミノを50個敷き詰めます。ドミノは縦または横向きに置きます。ドミノを敷き詰めたら、各ドミノを赤,青,緑のいずれかで塗ります。ただし、辺で隣接するドミノ同士が同じ色で塗られてはいけません。盤面の回転・反転によって一致する色の塗り方を別々に数え上げるとして、彩色の仕方は最大何通りあるでしょうか。また、盤面の回転・反転によって一致するドミノの置き方を別々に数え上げるとして、彩色の仕方の個数が最大になるようなドミノの敷き詰め方は何通りあるでしょうか。

AOJ 1169. 最強の呪文

問題文

問題文

考えたこと(箇条書き)

start --(S1, S2) --> v --T--> goalと行く場合を考える(vは注目した1頂点. S1,S2,Tは文字列)。
まず、S1 < S2 ⇒ S1 + T < S2 + Tは偽なので、各頂点で辞書順最小を作る方法では「最強の呪文」を作ることができない。(反例:S1 = "a", S2 = "ab", T = "c")
しかし|S1| = |S2|なら上の命題が真になるので、dp[文字数][頂点] = 辞書順最小とすることができる。(今いる頂点と文字数が同じなら, 辞書順最小が良い。)
よって、このDPをする。

最強の呪文が定まる場合、最強の呪文がなす経路はパス(同じ頂点を2度通らない)になることが示せる。(1つのサイクルを含む場合について証明すればよい)
よって, 最強の呪文(dp[--][goal])がサイクルを1つ以上含んでしまった時点で"NO"が確定する。
サイクルを1つ以上含むことで, パスの場合よりも強い呪文を作れるとすると、同じサイクルをn個含む「パスの場合よりも強い呪文」が任意の正整数nについて存在する。すなわち、
パスの場合よりも強い呪文が存在する ⇔ 経路長n未満の場合よりも強い, 経路長n以上2n以下の呪文が存在する
が成り立つ。よって、経路長2n以下まで, すなわち文字数12n以下まで調べれば十分である。
(もう少し考えると、パスの場合よりも強い呪文が存在する ⇔ 文字数6n未満の場合よりも強い, 文字数6n以上12n以下の呪文が存在する. も成り立つことが分かる)

はまった点

・dp[頂点] = 辞書順最小, を立てようとして嵌った。
・dp[枝を通った回数][頂点][長さ] = 辞書順最小, としてMLEした。
・dp[今までの文字数][今いる頂点] = 辞書順最小, としたが, 長さ昇順にループを回さないせいでTLEした。
・文字数 ≦ 6 * nまでしか考えていなくて、WAした。

…考察を詰める集中力がほしい。

ソースコード

#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;

int n, m, s, g;
vector<int> et[40];
vector<string> ec[40];
string dp[481][40];

int main() {
    int i, j, k;
    
    while (cin >> n >> m >> s >> g) {
        if (!n) break;
        for (i = 0; i < n; i++) { et[i].clear(); ec[i].clear(); }
        for (i = 0; i <= 12 * n; i++) { for (j = 0; j < n; j++) { dp[i][j] = "{"; } }
        for (i = 0; i < m; i++) {
            int a, b; string spell;
            cin >> a >> b >> spell;
            et[a].push_back(b);
            ec[a].push_back(spell);
        }
        
        dp[0][s] = "";
        for (i = 0; i < 12 * n; i++) {
            for (j = 0; j < n; j++) {
                for (k = 0; k < et[j].size(); k++) {
                    int v = et[j][k];
                    int len = ec[j][k].length();
                    if (i + len > 12 * n) continue;
                    dp[i + len][v] = min(dp[i + len][v], dp[i][j] + ec[j][k]);
                }
            }
        }
        
        int id = 0;
        for (i = 1; i <= 12 * n; i++) {
            if (dp[id][g] > dp[i][g]) id = i;
        }
        if (id >= 6 * n || dp[id][g] == "{") { cout << "NO" << endl; }
        else { cout << dp[id][g] << endl; }
    }
    return 0;
}

感想

複雑な問題設定だと思っていたが、いざ実装してみるととても簡潔に書けたので、良問だなあと思った。
発想というよりは、典型テク(知識, 発想・証明法)を上手く使いこなす系の問題な印象。

AOJ 2708 ABC Gene

問題概要

文字列sについて, 以下の置き換え操作ができる。

  • ‘A’ -> “ABC”
  • ‘B’ -> “ABC”
  • ‘C’ -> “ABC”

‘A’, ‘B’, ‘C'のみから構成される文字列S(|S|≦5000)が与えられるので, “ABC"を何回か操作することで文字列Sにできるか判定せよ。

解法1

逆手順を考える。
最後に操作「x -> “ABC"」をして, 文字列Sを作ったとする。(xは'A', ‘B’, ‘C'のどれか)
このとき、Sに含まれる"ABC"は、操作前は「x」になっている必要がある。

  • 証明:(xを含まない文字列) -> “ABC"と変化することはない。また, x -> "ABC"と変化するので、(xを含む文字列) -> "ABC"と変化する ⇒ 変化前の文字列長 = 1が成り立つ。
  • もちろん, (文字)(文字) -> 「~AB」「C~」といった変化をすることはない。また, “ABC” -> 操作 -> “ABC"と, そのままになることもあり得ない。

よって, 「x -> “ABC"」の逆元は「"ABC” -> x」となる。
(文字列Sに「x -> “ABC"」を適用したあと、「"ABC” -> x」を適用すると、文字列Sが得られる。)

あとは, xを求めればよい。実は、「"ABC"に何回か操作した文字列は必ず、'A'で始まり'C'で終わる」ことから, xは(必要条件で)一意に定まる。

具体的には、以下の場合分けにより, x(の候補)を特定できる。

  1. S = “ABC~"のとき

    • “ABC"に何回か操作した文字列は必ず、'A'で始まり'C'で終わる。よって, x = ‘A'である。
  2. S = “~ABC"のとき

    • 同様の理由で, x = ‘C'である。
  3. それ以外

    • もしx = ‘A'またはx = 'C'のときは, 操作「x -> “ABC"」をしたときに, 先頭または末尾に"ABC"ができてしまう。これは仮定より文字列Sではないので, x = 'B'である。

ここで, 「x = ~である」という文は、正確には「x = ~である必要がある」となる。
すなわち、「"ABC" -> x -> “ABC"」をしたときに、元の文字列が得られることを確かめる必要がある。
(ちなみに, 文字列Sに現れる文字の種類数が2以下の場合は, 解が存在しない。)

あとは頑張るとO(N2)くらいになる。

解法2

「x -> “ABC"」の逆元が「"ABC” -> x」であることを求めてしまえば、あとは適当に逆再生できる。
(文字種が2種類以下⇒No, 「"ABC" -> x -> “ABC"」をしてSが得られる -> 「"ABC” -> x」をSに適用する, を繰り返すだけ)
こちらもO(N2)くらいである。ただし, 計算量の証明には解法1が用いられている。今回はこちらを実装した。

コード

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

//x -> "ABC"
string operate(string s, char x) {
    string t;
    for (int i = 0; i < s.length(); i++) {
        if (s[i] == x) { t += "ABC"; }
        else { t += s[i]; }
    }
    return t;
}

//"ABC" -> x
string inverse(string &s, char x) {
    string t;
    for (int i = 0; i < s.length(); ) {
        if (i + 2 < s.length() && s[i] == 'A' && s[i + 1] == 'B' && s[i + 2] == 'C') {
            t += x;
            i += 3;
        }
        else {
            t += s[i];
            i++;
        }
    }
    return t;
}

//is all char appeared?
bool check(string &s) {
    int cnt[3] = {0};
    for (int i = 0; i < s.length(); i++) {
        cnt[s[i] - 'A']++;
    }
    return (cnt[0] > 0 && cnt[1] > 0 && cnt[2] > 0);
}

int main() {
    string s;
    cin >> s;
    
    while (true) {
        if (!check(s)) { break; }
        if (s == "ABC") { cout << "Yes" << endl; return 0; }
        
        char last = 'z';
        for (char x = 'A'; x <= 'C'; x++) {
            if (operate(inverse(s, x), x) == s) {
                last = x;
                break;
            }
        }
        if (last == 'z') break;
        
        s = inverse(s, last);
    }
    
    cout << "No" << endl;
    return 0;
}

CodeFestival2015本選の問題(解法メモ)

備忘録に解法に至るアプローチを貼っておきます。

 

A: 575判定。cin >> s >> t >> u; cout << (s.length()==5&&t.length()==5&&u.length()==5) ? "YES":"NO" << endl;

 

B: N≧2における合計値の確率分布では平均が一番盛り上がっている。N=1では均一なので1が答え。

 

C: 前から文字列を見ていき、"01"か"10"が見つかったら即座にペアにしていって良い。(010…とあったとき、0「10」…としても「01」0としても前3文字までの分解数は同じ&0「10」とすると残った0とペアになる1が絶対なくなるが「01」0とすれば残った0とペアになる1があるかもしれない -> そのときは得!)などと考えたが、詳しい証明は分からない。

同じ文字を複数のペアに割り振らないように注意する。

 

D: 

①いつだれがどのスイッチを押すかと考えると闇 -> 人数だけ求めればよい!

②N点取る場合…区間が最大M個かぶっていたらM人以上必要、また、M人で十分。

③N-1点取る is 全区間 - {ある一つの区間}

④(ある区間にXを足す, 区間のMaxを求める)というクエリが沢山来る -> StarrySky木

もしくは、N点取る場合に区間が最大M個かぶっているとき、答えがMかM-1であることを利用すればO(N)。(M個区間が被っている時間の(左端, 右端)を覆えるかどうかで判定できる)

 

E:

答えはそんなに長くならない -> 反復深化で全探索しよう!判定は色んな入力を元のコード, 生成したコードの2つに与えて、出力の一致を確かめるなど…。

 

F:

①サイクルにする(グラフ問題にする)

②頂点と辺の通る回数に注目する

③辺を通る回数を考える

④連結判定

 

G:

・列の問題に落とせるらしい??

 

H:

・3枚重ねるのはタブー

・重なって損した量(ペナルティー)で辺を貼ると、最短経路問題になるらしい

競プロ始めたときに埋めた問題

について少しメモしておきます。

競プロを本格的に始めたのはARC(AtCoder Regulars Contest)18からです。
(当時は、B問題のバグに苦しめられていた。)

-1.5年~-0.5年

・JOI予選1~3問目を新しい年のものから埋めていました
(実力、JOI予選200点レベル)

・20行のコードのデバッグに20分以上かけてしまうこともしばしばありました。

-0.5年~0年

JOI予選4~6問目埋め

・このとき、AOJでは30問程度解いていました
・6問目は、2006, 2007年度の予選のものしか解けていませんでした
・一番印象を受けた問題は、「1年生」です。
・この問題をきっかけに、競技プログラミングが好きになりました

JOI本選1~3問目埋め

・楽しい問題、良質な解説…これに手を付けない手はありませんでした
・100枚1冊のレポート用紙に考察を書きこんでいたこと、今でも覚えています
(・2次元累積和に自力で気づいたり、JOI2008本選5番が解けたときは感動した)
(実力、AtCoderRegularsContestB問題が解けるかどうか)

0年~

AtCoderの定期コンテストに参加
・PCK, JOI, ICPC, JAGあたりを中心的に埋めた(AOJ200問解きました)
TopCoder, Codeforces…(深夜コンテストへの参加は不可能でした)
・YukiCoder☆2(1/4くらい面白そうな問題をやりました)

最近

・留年の危機()が迫ってきたので、TopCoder-Div1Easyに絞って練習しています。

(実力、CodeFestival本選5完、ARC2完、TopCoder-緑(1100付近))

最後に

文章がひどいですね…

【備忘録】SRMDiv1Easyを20問埋めてみたら

はじめに

最近、SRMDiv1Easyを20問解いたので、そろそろ何か書き残しておこうと思います。
殆ど自分用なので、かなりひどい文章です。ご容赦ください。

1.SRMを選んだ理由

・読みやすい問題文が多い
・実装が軽い
・良問が多い
・難易度が安定している
・↑のこともあり、TopCoderのレートを上げたくなった
・時間で得点が変わる&英語だから緊張感を得られる

2.Div1Easyを選んだ理由

・最初の方針が合っていれば解ける/嵌ると解けない
 →方針を合わせる練習になる
・1~2時間で解ける問題が半分くらいある
 →ギリギリ解けるのが面白い
・想定解は自分でも分かるくらい簡単
 →解説で分かると悔しい
・実装が軽い
 →お手軽

3.解くときの方針

手順
①1~2時間解いてみる
②解けなければブログ解説の解法だけを見る
③理解できなければ実装もチラ見する
④理解した(クリアだ!簡単だ!と思えた)ら実装して通す(必ず通す)

心得
・証明不能な貪欲は大体嘘貪欲
・簡単な考察&軽実装で解ける(そうでないものは捨てる)
・必ず解けると思う、ただしあきらめも肝心(無理に通そうとしない)
・本番の環境に近づけるため、新しい問題からやる

4.埋める前に思ったこと

・闇雲に埋めてもダメ
・意識して埋めてれば実力は上がりそう
・どのくらい??
・というより長続きするの?(AOJ埋めは3日坊主で終わった)

5.埋めてみて思ったこと

・実力は微妙に上がった気がする
→考察の個数が「1つ」から「3つ」くらいになった
→埋める前に嵌った問題は、やはり嵌ったので微妙
・ギリ解ける問題解いてる時は勝手に集中してた
・細々とだが続いている
SRM(するめ)には中毒性がある

6.最後に

とりあえず、SRM600台は埋める予定。
これで解ける問題が増えたらラッキーなんだけど世の中そんなに甘くなさそうだ。

SRM 658 div1 easy: OddEvenTree

問題概要

N頂点(頂点0~N-1)の木の距離行列xが与えられる。
0 <= i, j <= N-1を満たす全ての(i, j)組について
x[i][j] = 'O'ならば頂点(i, j)間が奇数長
x[i][j] = 'E'ならば頂点(i, j)間が偶数長
が成り立つような、木は存在するだろうか。
存在しなければ{-1}を返し、存在すれば、そのような木を一つ出力せよ。

解法

木が存在するためには、以下の条件を全て満たす必要がある。
・xが対称行列
・全てのiについて、x[i][i] == 'E'
・任意の3頂点A, B, Cについて、AB間の距離 + BC間の距離 + CA間の距離が偶数
・一つ以上'O'が存在
なお、これは十分条件ではない(テストケース88は、これらすべてを満たすが、{-1}を返すべきケース)

上の条件をすべて満たしたとき、木の構築をする。
木の構築は、以下の貪欲アルゴリズムで行い、全部つなげなければ{-1}を返すようにしたら通った。
・頂点0を接続済み頂点にする。他の頂点は「接続済み頂点」ではない。
・全ての頂点を「接続済み頂点」にするか、つなげなくなるまで以下を繰り返す。
 ・「接続済み頂点A」と「接続済みでない頂点B」を見つけたら、頂点AとBを結ぶ。
 ・見つからなければ、{-1}を返す。

・このアルゴリズムを使って木を構築したとき、条件を満たさない木が出来たり、条件を満たす木が存在するのに{-1}を返したり しなかった。理由は良くわからない。

//必要条件だけで枝刈り&条件を満たしたらすぐにつなぐ、という嘘っぽいあれ。
//木の構築中に矛盾が生じたらfalseでも良かったけど、見落としがあると不味いので。
#include <iostream>
#include <vector>
#include <string>
using namespace std;

class OddEvenTree{
    public:
    vector<int> False(){
        vector<int> ret;
        ret.push_back(-1);
        return ret;
    }
    vector<int> getTree(vector<string> x)
    {
        int i, j, k;
        int n = x.size();
        int Ecnt = 0;
        
        for( i = 0; i < n; i++ ){
            for( j = 0; j < n; j++ ){
                if( x[i][j] != x[j][i] )
                    return False();
                if( i == j && x[i][i] == 'O' )
                    return False();
                Ecnt += (x[i][j] == 'E');
            }
        }
        if( Ecnt == 0 ){
            return False();
        }
        for( i = 0; i < n; i++ ){
            for( j = 0; j < n; j++ ){
                for( k = 0; k < n; k++ ){
                    int d = (x[i][j] == 'O') + (x[j][k] == 'O') + (x[k][i] == 'O');
                    if( d % 2 == 1 )
                        return False();
                }
            }
        }
        
        //木の構築(エラーは検出済みなので、これで構築できなかったら最適な構築ではない)
        //ばばーん
        //(貪欲に構築しているため、条件を満たさない木が出来るかもしれない…)
        bool used[50] = {false};
        int sz = 1;
        vector<int> ans;
        
        used[0] = true;
        
        while( sz < n ){
            bool okflag = false;
            for( i = 0; i < n; i++ ){
                for( j = 0; j < n; j++ ){
                    if( used[i] && !used[j] && x[i][j] == 'O' ){
                        used[j] = true;
                        ans.push_back(i); ans.push_back(j);
                        okflag = true;
                        sz++;
                    }
                }
            }
            if( !okflag ){
                return False();
            }
        }
        return ans;
    }
};

提出コードのコメントがひどい