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;
    }
};

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

JOI模擬予選2015-2016参加記

JOI模擬予選に参加しました。
難易度はおおむね例年の予選と同じだが、問1が若干難しく、問3が簡単な印象。
備忘録ということで、コードを貼りつけておきます。

1 けんだま

問題

玉がA個、本体がB個ある。玉を1個増やすのにC[円]かかり、本体を1個増やすのにD[円]かかる。
予算X[円]で最大いくつのけん玉を作れるか。

解法

y個のけん玉を作るとする。
このときかかる金額は、C * max(0, y - A) + D * max(0, y - B)[円]である。
したがって、これがX以下となるようなyの最大値を出力すればよい。

コード

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

signed main()
{
    int a, b, c, d, x;
    int y;
    
    cin >> a >> b >> c >> d >> x;
    for( y = 0; y < 20000; y++ ){
        int cost = c * max(0, y - a) + d * max(0, y - b);
        if( cost > x )
            break;
    }
    cout << y - 1 << endl;
    return 0;
}

signedはintみたいなもの。

2 回文

問題

N文字の文字列S = s[1]s[2]…s[N]が与えられる。
3つの文字列
{s[i]s[i+1]…s[i+X-1]}
{s[i+X]s[i+X+1]…s[i+X+Y-1]}
{s[i+X+Y]s[i+X+Y+1]…s[i+2*X+Y]}
が全て回文となるようなiの個数を計算せよ。

解法

愚直にシミュレーションします。
文字列{s[l]s[l+1]…s[r-1]}が回文かどうかを判定する関数を作ると実装が楽でした。

コード

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

int n, x, y;
string s;

bool ok( int l, int r ){
    string t, u;
    for( int i = l; i < r; i++ )
        t += s[i];
    for( int i = r - 1; i >= l; i-- )
        u += s[i];
    if( t == u )
        return true;
    return false;
}

signed main()
{
    int i;
    
    cin >> n >> x >> y;
    cin >> s;
    
    int ans = 0;
    for( i = 0; i < s.length() - 2 * x - y + 1; i++ ){
        if( ok(i, i + x) && ok(i + x, i + x + y) && ok(i + x + y, i + 2*x + y) )
            ans++;
    }
    cout << ans << endl;
    return 0;
}

3 渦巻きに歩く問題

問題

H行W列のグリッドがある。各マスには(1, 1), (H, W)などの座標が付けられている。
私は左上のマス(1, 1)から右渦巻きに移動していき、通ったマスに色を塗ることを繰り返す。
最初は青を塗り、次は赤、その次は青、というように青と赤を交互に塗っていく。
このとき、N個の座標(r1, c1), (r2, c2), …, (rN, cN)にあるマスはそれぞれ何色で塗られていたでしょうか。
青で塗られていたマスの数, 赤で塗られていたマスの数を空白区切りで出力せよ。
(制約)
1 <= H, W <= 109
1 <= N <= 1000

解法

ラフな考え

手で試すと、市松模様になることが分かる。
したがって、マスの座標を(a, b)とするとそのマスはa + bが偶数なら青、a + bが奇数なら赤で塗られている。

厳密な考え

移動先のマスの座標を(a, b)すると、移動するごとにa + bの偶奇が入れ替わる。
塗る色も入れ替わる。最初はa + bは偶数で塗る色は青である。
したがって、マスの座標を(a, b)とするとそのマスはa + bが偶数なら青、a + bが奇数なら赤で塗られている。
というわけで、(ri + ci) % 2 == 0となるiの個数、(ri + ci) % 2 == 1となるiの個数を計算すればよい。

コード

#include <iostream>
using namespace std;

int h, w, n;
int r, c;

signed main()
{
    int i;
    cin >> h >> w >> n;
    
    int bcnt = 0;
    int rcnt = 0;
    for( i = 0; i < n; i++ ){
        cin >> r >> c;
        if( (r + c) % 2 == 0 )
            bcnt++;
        else
            rcnt++;
    }
    cout << bcnt << " " << rcnt << endl;
    return 0;
}

4 鉄道に乗る問題

問題

駅1, 2, 3, …, Nがこの順で並んでいる。
私は時刻0に駅Sにいる。いくら時間がかかってもよいので駅Tにつきたい。そのような電車の乗り方が何通りあるかを計算せよ。
ただし、乗り換え時間は無視できるものとする。電車の情報は以下の通り。
電車はM本ある。
i本目の電車は時刻A[i]に駅B[i]を発車し、駅B[i] + C[i], B[i] + 2C[i], …の順で止まる。なお、到着時刻(=発車時刻)はそれぞれA[i] + C[i], A[i] + 2C[i], …となる。
(制約)
1 <= N <= 100
1 <= M <= 100
1 <= A[i] <= 100
1 <= B[i] <= N
1 <= B[i] + C[i] <= N
(C[i]が負になる場合もある)

解法

次に乗れる電車は、現時点での(時刻, 駅, さきほど降りた電車)から分かる。
また、この3つを持っておけば、電車Xに乗って駅Yで降りた直後の(時刻, 駅, さきほど降りた電車)も分かる。
今いる駅 = Tになれば駅に行けたことが分かる。
したがって、(時刻, 駅, さきほど降りた電車の番号)を引数にした再帰関数を組むことができる。
ただし、再帰関数を愚直に組むと時間超過するので、一工夫する。
同じ(時刻, 駅, さきほど降りた電車の番号)場合の再計算が無駄なので、最初に計算した結果を配列にメモしておき、
再び同じ(時刻, 駅, さきほど降りた電車の番号)に遷移したときは、その結果を返すようにする。
つまり、(時刻, 駅, さきほど降りた電車の番号)を状態としてDPする。
遷移(X, Yを決めるところ)は複雑なので注意。

コード

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

int n, m, s, t;
int a[100], b[100], c[100];

int dep;                //デバッグ用(探索の深さ)
int dp[201][101][100];  //dfs(jikoku, eki, ressya)の返した値

//「時刻jikokuに駅ekiにいて列車ressyaから降りた」状態から「駅tへ行く方法の数」を返す
int dfs( int jikoku, int eki, int ressya ){
    int i, j;
    int ret = 0;

    /*for( i = 0; i < dep; i++ ) cout << "・";
    cout << " 時刻" << jikoku << " 駅" << eki;
    cout << endl;*/
    
    if( ressya >= 0 && dp[jikoku][eki][ressya] != -1 )
        return dp[jikoku][eki][ressya];
        
    if( eki == t )
        ret++;  //駅tについたから1通り加算する
    
    for( i = 0; i < m; i++ ){
        //降りた直後に同じ電車には乗れない
        if( i == ressya )
            continue;
        //駅ekiに停車しない電車には乗れない
        if( (eki - b[i]) / c[i] < 0 || abs(eki - b[i]) % abs(c[i]) != 0 )
            continue;
        //駅ekiを通過してしまった電車には乗れない
        if( jikoku > a[i] + abs(b[i] - eki) )
            continue;
        //電車iに乗って駅jで降りる
        for( j = eki + c[i]; 1 <= j && j <= n; j += c[i] ){
            dep++;
            ret += dfs(a[i] + abs(b[i] - j), j, i);//次の駅からの行き方を足す
            ret %= 100000;      //逐次余りを取る
            dep--;
        }
    }
    dp[jikoku][eki][ressya] = ret;
    return ret;
}

signed main()
{
    int i;
    cin >> n >> m >> s >> t;
    for( i = 0; i < m; i++ ){
        cin >> a[i] >> b[i] >> c[i];
    }
    
    for( i = 0; i < 201; i++ ){
        for( int j = 0; j < n + 1; j++ ){
            for( int k = 0; k < m; k++ ){
                dp[i][j][k] = -1;
            }
        }
    }
                
    cout << dfs(0, s, -1) << endl;
    return 0;
}

もっときれいなコードを書きたかった。
想定解とは少し違うらしい。

5 楽しい楽しい花の水やり

問題

花1, 2, …, Nがある。0日目の花iのみずみずしさはA[i]である。花のみずみずしさは日付が変わる瞬間にB[i]減少する。
みずみずしさが0以下になった花は枯れてしまう。
私は好きな時刻に何度でも「みずやり」をすることができる。「みずやり」は次の2ステップで行われる。
1.整数iを決める
2.花i - K, i - K + 1, …, i + KのみずみずしさをC増やす。
T日目にどの花も枯れていないようにしたい。しかし「みずやり」も重労働なので「みずやり」の回数もなるべく少なくしたい。
「みずやり」の回数の最小値を計算せよ。
(制約)
1 <= N <= 5 * 10 ^ 6
1 <= T <= 10 ^ 7
1 <= K <= N / 2
1 <= A[i], B[i] <= 10 ^ 7
1 <= C <= 10 ^ 7
なお、a ^ bはaをb回かけたものである。
(補足)
A[i], B[i]は乱数で生成する。生成の仕方はコードを見て察してください。

解法

まず、全ての入力はlong long型変数で管理したほうが良い。

考察1

水やりを全く行わなかったら、花iのみずみずしさは、A[i] - B[i] * Tとなる。
花iにX[i]水がかかったら、花iのみずみずしさは、水やりをしたタイミングによらず
A[i] - B[i] * T + C * X[i]
となる。
少なくともこれが0を超えている必要がある。逆に、これが0になるような最小のX[i]をMx[i]としたとき、
花iにはMx[i]回以上水がかかれば良い。
水やりは0日目にまとめて行おう。なお、水やりの中心位置は
花1 + K, 2 + K, …のようにずらしていくのが最善である。
具体的には、
花1~1 + 2Kに「みずやり」をする。
花1にMx[1]回以上水をやったら(花1が枯れなくなったら)花2~2 + 2Kに「みずやり」をする。
花2にMx[2]回以上水をやったら(花2が枯れなくなったら)花3~3 + 2Kに「みずやり」をする。

を繰り返せばよい。
これを愚直にやるとO(NK)で答えを計算できる。

考察2

上記の「みずやり」シミュレーションを高速化したい。具体的には
・X[i]の値を求める
・X[i+1]~X[i+2K]の値をX[i]減らす
というクエリを高速に処理したい。
これは区間加算1点クエリのセグメントツリーでできる。
もしくは累積和を行えばよい。私はセグメントツリーで解いたが、累積和(いもす法)を使えば
実行が速く実装も楽である。

コード

#include <iostream>
#include <algorithm>
#define int long long   //intがlonglongに置換される
using namespace std;

int N, K, C, T;
int Sa, Xa, Ya, Za;
int Sb, Xb, Yb, Zb;
void input(){
    cin >> N >> K >> C >> T;
    cin >> Sa >> Xa >> Ya >> Za;
    cin >> Sb >> Xb >> Yb >> Zb;
}
void generate(int A[], int B[]){
    for( int i = 1; i <= N; i++ ){
        A[i] = Sa + 1;
        B[i] = Sb + 1;
        Sa = (Xa * Sa + Ya) % Za;
        Sb = (Xb * Sb + Yb) % Zb;
    }
}

int A[5000001], B[5000001];
int X[5000001]; //X[i] = 花iに水をかける回数の最小値

const int depth = 23;
class Segtree{
public:
    int data[20000000];
    void init(){
        for( int i = 0; i < 20000000; i++ ) data[i] = 0;
    }
    void add( int l, int r, int v, int a = 0, int b = (1 << depth) - 1, int ind = 0, int dep = 0 ){
        if( dep > depth ) return;
        if( b < l || a > r ) return;
        if( l <= a && b <= r ){
            data[ind] += v;
        }
        else{
            add( l, r, v, a, a + (b - a + 1) / 2 - 1, ind * 2 + 1, dep + 1 );
            add( l, r, v, a + (b - a + 1) / 2, b, ind * 2 + 2, dep + 1 );
        }
    }
    int getNum( int x ){
        int ret = 0;
        x += (1 << depth) - 1;
        ret += data[x];
        while( x > 0 ){
            x = (x - 1) / 2;
            ret += data[x];
        }
        return ret;
    }
}seg;

void generateX(){
    for( int i = 1; i <= N; i++ ){
        //A[i] - B[i] * T + C * X[i] > 0となるX[i]の最小値を求める
        X[i] = max((int)0, (B[i] * T - A[i]) / C + 1);
    }
}

void segInit(){
    seg.init();
    for( int i = 1; i <= N; i++ ){
        seg.add(i, i, X[i]);
    }
}

//花1 + Kを中心に水をかける→花2 + Kを中心に水をかける→…
//水やり回数を返す
int solve(){
    int center;
    int ans = 0;
    
    for( center = 1 + K; center <= N + K; center++ ){
        int left = center - K;
        int right = min(N, center + K);
        
        //max(0, X[left])回水やりをする
        //①max(0, X[left])を求める
        int kaisu = max((int)0, seg.getNum(left));
        
        //②答えを更新
        ans += kaisu;
        
        //③X[left]~X[right]に-max(0, X[left])足す
        seg.add(left, right, -max((int)0, kaisu));
    }
    return ans;
}

signed main()
{
    input();
    generate(A, B);
    generateX();
    segInit();
    cout << solve() << endl;
    return 0;
}

高級な道具を使ってしまった。

6 完全に忘れました

ごめんなさい

感想

JOI予選模擬にもかかわらずガチ参加できるほど良問ぞろいでした。
部外者が勝手に参加してすみませんでした。
運営陣は素晴らしい。はっきりわかんだね。