競プロやります。

競プロを頑張るブログです。

AtCoder Beginner Contest 129

D - Lamp

https://atcoder.jp/contests/abc129/tasks/abc129_d

方針

上から  h 行目、左から  w 列目のマスを  (h,w) と書きます。 (h,w) に明かりを設置したときに、

  • 左右方向に照らせるマスの数(自身を含む)を  LR(h,w)
  • 上下方向で照らせるマスの数(自身を含む)を  UD(h,w)

とします。答えは  LR(h,w) + UD(h,w) -1 の最大値です。最後の  -1 は、自身をダブルカウントしている分です。

 LR(h,w) を求めるには、まず障害物のないマスが左右方向につながった領域の、左端を見つけます。そこから障害物に達する or グリッドをはみ出すまで右側へ移動しながら、この領域の幅を数えていきます。こうして幅  dx を求めてから、 LR(h,w) = LR(h, w+1) =  \dots  = LR(h, w+dx-1) = dx と代入していけばよいです。

あるマスが障害物のない領域の左端であるための条件は、「 (h,w) に障害物が無い」かつ 「 (h,w-1) に障害物がある or  (h,w) がグリッドの左端( w=0)」となります。

ソースコード

#include<bits/stdc++.h>
using namespace std;

int H, W;
vector<vector<int>> LR, UD;
vector<string> S;

int dfsX(int h, int w, int dx){
    // (h,w) :探索の現在位置。
    //  dx   :障害物のない領域の左端からこのマスまでの幅。
    if(w+1>=W || S[h][w+1]=='#'){
        // これ以上右へ進めない場合は dx を返す。
        LR[h][w] = dx;
        return dx;
    }

    // まだ右へ行ける場合は、右へ移動し dx を一つ増やす。
    LR[h][w] = dfsX(h,w+1, dx+1);
    return LR[h][w];
}

int dfsY(int h, int w, int dy){
    // dfsX と同じことを上下方向にやる
    if(h+1>=H || S[h+1][w]=='#'){
        UD[h][w] = dy;
        return dy;
    }
    UD[h][w] = dfsY(h+1,w, dy+1);
    return UD[h][w];
}

int main(){
    cin >> H >> W;
    S = vector<string>(H);
    for(int i=0; i<H; i++){
        cin >> S[i];
    }

    // LR と UD を求める。
    LR = vector<vector<int>>(H,vector<int>(W,0));
    UD = vector<vector<int>>(H,vector<int>(W,0));
    for(int h=0; h<H; h++) for(int w=0; w<W; w++){
        // (h,w) が障害物の無い領域の左端 or 上端のときに探索を始める。
        if(S[h][w]=='.'){
            if(w==0 || S[h][w-1]=='#'){
                LR[h][w] = dfsX(h,w,1);
            }
            if(h==0 || S[h-1][w]=='#'){
                UD[h][w] = dfsY(h,w,1);
            }
        }
    }
    
    int ans = 0;
    for(int h=0; h<H; h++) for(int w=0; w<W; w++){
        ans = max(ans, LR[h][w] + UD[h][w] - 1);
    }
    cout << ans << endl;
}

反省

  • 左右や上下を LR や UD としてまとめるのではなく、公式の解説のように別々に取り扱った方が楽だった気がします。
  • 今回実装した dfsX のような再帰的な処理は書き慣れていないのですが、別に無理に再起を使わなくても書けましたね。最初に浮かんだ方針で実装してしまう前に、一呼吸置けるようになりたいです。

E - Sum Equals Xor

https://atcoder.jp/contests/abc129/tasks/abc129_e

方針

 L の二進数表記の、上から数えて  i ビット目を  L_i と書きます。ここで index は 0 始まりとします。例えば  L = 10110 のとき、  L_0 = 1 L_4 = 0 となります。  a_i,~b_i も同様に定義します。また、 L を2進数表記したときのビット数を  N とします。

まず  a+b = a \oplus b となるとき、 a,~b の各ビットに現れる 1 は高々 1 個になります。すなわち  (a_i,b_i)=(0,0),(1,0),(0,1) です。これを前提に、 a+b \leq L となる条件を考えます。
ここで  L_0,~L_1,~\dots, L_i の中にある 1 の数を  c_i としておきます。

(1)  a+b = L のとき
 L_i = a_i + b_i = a_i \oplus b_i となればよいです。これはすなわち

  •  L_i = 0 のとき  (a_i, b_i) = (0,0)
  •  L_i = 1 のとき  (a_i, b_i) = (1,0),~(0,1)

となることと同値です。つまり  L_i = 0 となるビットは  (a_i,~b_i) = (0,0) 固定で、 L_i = 1 のビットは  (a_i, b_i) = (1,0),~(0,1) の 2 通りが許されることになりますから、適切な  (a, b) の組み合わせは  2^{c_{N-1}} 通りです。

(2)  a+b < L のとき
このとき  a+b L を上位ビットから比較していくと、あるところまでは  a_i + b_i = L_i が成立し続けますが、どこか ( k ビット目とします) で  a_k + b_k < L_k となります。そして、k ビット目よりも下位では  a_i,~b_i の組み合わせが何であるかに関わらず、  a+b < L は変わらず成立します。
よって、  a+b < L となる条件は、適当な  k が存在して以下を満たすことです:

  •  a_i + b_i = L_i~(0 \leq i < k, ~ k > 0)
  •  L_k = 1 かつ  a_k + b_k = 0

これは例えば10進数で  A= 142987 B = 145125 と比較するとき、上位 2 桁は一致しているが、3桁目が  A の方が小さいため、 A < B と判断できることの2進数版になります。

ではこのときに  (a_i, b_i) の組合わせが何通りになるかを考えます。

(2-1)  k=0 のとき
問題文の条件より  L_0 = 1 ですから、 a_0 + b_0 = 0 となってしまえば、 a,~b の残りのビット :  (a_1, b_1) (a_{N-1}, b_{N-1}) に関わらず  a+b < L が成立します。
よって、0ビット目は  (a_0, b_0) = (0,0) で固定。残り  N-1 ビットは  (a_i,b_i)=(0,0),(1,0),(0,1) のどれでもよいので、適切な  (a,b) の組み合わせは  3^{N-1} 通りとなります。

(2-2)  0 < k < N-1 のとき
このとき、 L_k = 1 でなければいけません。この元で、

  • 0 ビット目から k-1 ビット目までは  a_i + b_i = L_i が成立するので、(1) と同じように考えて  2^{c_{k-1}} 通りになります。
  • k ビット目は  L_k=1 かつ  a_k + b_k = 0 ですから、  (a_k, b_k) = (0,0) の1通りです。
  • k+1 ~ N-1 ビット目は、 (a_i,b_i)=(0,0),(1,0),(0,1) であればよいので、 3^{N-k-1} 通りです。

以上合わせて、 2^{c_{k-1}} \times 3^{N-k-1} 通りです。

(2-3)  k=N-1 のとき
 L_k = L_{N-1} = 1 でなければいけません。このときは、(2-2) の3つ目の条件のみ無視すればよく、 2^{c_{N-2}} 通りとなります。
ただし、 k=N-1 のとき (2-2) は  2^{c_{k-1}} \times 3^{N-k-1} = 2^{c_{N-2}} ですから、(2-2)の式はそのまま使えます。


以上合わせて、 d_i = 2^{c_i} として答えは次式で求められます。ここでシグマは  L_k=1 なる  k についてのみ和をとります。
  \displaystyle 2^{c_{N-1}} + 3^{N-1} + \sum_{k=1}^{N-1} 2^{c_{k-1}} \times 3^{N-k-1} = d_{N-1} + 3^{N-1} + \sum_{k=1}^{N-1} d_{k-1} 3^{N-k-1}

ソースコード

シグマの中の 3 のべき乗は繰り返し二乗法で求めました。

#include<bits/stdc++.h>
#define ll long long
using namespace std;

ll mod_pow(ll n, ll p, ll mod){
    // return (n^p) % mod
    if(p==0) return 1;
    ll res = mod_pow(n*n%mod, p/2, mod);
    if(p%2==1) res = res * n % mod;
    return res;
}

int main(){
    string L;  cin >> L;
    ll N = L.length();
    ll mod = 1e9+7;

    // d を求める。
    // d[i] = 2^(L[0]~L[i] の中にある 1 の数) 
    vector<ll> d(N,0);
    d[0] = 2;
    for(int i=1; i<N; i++){
        if(L[i]=='0') d[i] = d[i-1];
        else d[i] = 2*d[i-1]%mod;
    }

    ll ans = 0;

    // (1) の場合
    ans += d[N-1];

    // (2) の場合
    for(int k=0; k<N; k++){
        if(L[k]=='0') continue;  // L[k] = 1 についてのみ和を取る

        ll tmp = 1;
        if(k>0){
            // k=0 のときは (2-2) の 1 つ目の条件を無視する
            tmp = tmp*d[k-1]%mod;
        }
        // (2-2) の 3 つ目の条件
        tmp = tmp*mod_pow(3, N-k-1, mod)%mod;

        ans = (ans +tmp)%mod;
    }    
    cout << ans << endl;
}