桁DPの実装
桁に着目して状態をまとめてDPするテク
取り扱える条件の代表的な例としては 以下の数,桁和,倍数 などである
桁DP自体の解説は以下のサイトなどがわかりやすい
Digit DP 入門 by とーらすさん
桁 DP の思想 〜 K 以下の整数を走査するとはどういうことか 〜 by drkenさん
解くのにかかる時間,思考パートを減らすために自分が実装をどうやるかまとめておく.
実装
上の桁から順に見ていく方法と下の桁から順に見ていく方法が存在している.以下について取り扱う場合は上から,繰り上がりなどを扱う場合は下から見る方法がよさそう.
自分は上から見る実装が好きなので主にそっちについて書きます.
10進法で条件(3の倍数)を満たすs以下の数の個数を数えるコードが以下のものである.この形が基本で問題の条件に合わせて書き換えていく.
string s; cin >> s; ll n = s.size(); // dp[i桁目][以下が確定したか][mod 3] dp[0][0][0] = 1; REP(i, n) REP(j, 3) REP(k, 2) { ll lim = k==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = (j+d) % 3, nk = k || d<lim; dp[i+1][nj][nk] += dp[i][j][k]; } } cout << dp[n][0][0] + dp[n][1][0] << endl;
問題に応じて考えるやつ
- 条件をDPのkeyにどうやってもたせるか?
以下が確定したか,桁和,今までに現れた数 など問題に合わせてDPテーブルのkeyを考える - 条件を満たす数の個数以外を求める場合
← へどのように遷移するのか変える
考える要素がほとんど無でできるやつ
- 進法以外を扱う
limの設定をに合わせる - で条件を満たす数の個数を数える
を数える関数 をつくって とする (には注意) - で条件を満たす数の個数を数える
桁DPは基本的に を数えるので-1する - s以上について取り扱う
を に変えればよい
問題例
以上の実装にしたがって問題を解いていく.競技プログラミングにおける桁DP問題まとめ by hamayanhamayanさん を主に解いています.
禁止された数字
「今までに4か9が現れたか?」という条件をもてばよい.
auto solve = [&](string s) { const ll n = s.size(); memset(dp, 0, sizeof(dp)); dp[0][0][0] = 1; REP(i, n) REP(j, 2) REP(k, 2) { ll lim = j==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = j || d<lim, nk = k || d==4 || d==9; dp[i+1][nj][nk] += dp[i][j][k]; } } return dp[n][0][1] + dp[n][1][1]; }; cout << solve(to_string(b)) - solve(to_string(a-1)) << endl;
TDPC 数
Xの倍数 で 0 と言い換えると見通しがよくなるパターンが多い.「桁和 mod D」という条件をもてばよい.
dp[0][0][0] = 1; REP(i, n) REP(j, x) REP(k, 2) { ll lim = k==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = (j+d) % x, nk = k || d<lim; dp[i+1][nj][nk] += dp[i][j][k]; } } cout << dp[n][0][0] + dp[n][0][1] - 1 << endl;
ごちうさ数
「前4桁がなにか」「51?3がすでに現れたか」という条件を持てばよい.
dp[0][0][0][0] = 1; REP(i, n) REP(j, 10000) REP(k, 2) REP(l, 2) { ll lim = k==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = (j*10 + d) % 10000, nk = k || d<lim, nl = l; if(nj/100 == 51 && nj%10 == 3) nl |= 1; dp[i+1][nj][nk][nl] += dp[i][j][k][l]; } } ll ret = 0; REP(i, 10000) REP(j, 2) ret += dp[n][i][j][1]; cout << ret << endl;
1
今見ている 桁目が となる数の個数について考える.これは (桁目までの個数) (桁目以降の個数) となる.桁目までの個数 は で数えている.桁目以降の個数は
- (以下が確定しない)
sの桁目以降のsuffix - (以下が確定する)
となる.
p10[0] = 1; FOR(i, 1, 11) p10[i] = p10[i-1] * 10; for(ll i=n-1; i>=0; --i) suf[i] = suf[i+1] + p10[n-1-i] * (s[i]-'0'); ll ret = 0; dp[0][0] = 1; REP(i, n) REP(j, 2) { ll lim = j==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = j || d<lim; dp[i+1][nj] += dp[i][j]; if(d==1) { ll num = d==lim ? suf[i+1]+1 : p10[n-1-i]; ret += dp[i][j] * num; } } } cout << ret << endl;
壊れた電卓
以下/以上で達成できる最小の差をそれぞれ求める.
「現れた数字の集合」をbit演算を用いて,leading-zeroの0を現れた数字に含めてはいけないので「leading-zeroか」を DPのkeyに持つ.
桁目に が現れるとき,それによって との差がどれだけ変わるか?に沿ってDPの遷移をする.
p10[0] = 1; FOR(i, 1, 20) p10[i] = p10[i-1] * 10; ll ret = INF; // s以下 { REP(i, 20) REP(j, 1LL<<11) REP(k, 2) REP(l, 2) dp[i][j][k][l] = INF; dp[0][0][0][0] = stol(s); REP(i, n) REP(j, 1LL<<11) REP(k, 2) REP(l, 2) { ll lim = k==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = j, nk = k || d<lim, nl = l || d!=0; if(nl==1) nj |= 1LL<<d; chmin(dp[i+1][nj][nk][nl], dp[i][j][k][l] - p10[n-1-i] * d); } } REP(i, 1LL<<11) REP(j, 2) REP(k, 2) { if(__builtin_popcount(i) > x) continue; chmin(ret, dp[n][i][j][k]); } } // s以上 { REP(i, 20) REP(j, 1LL<<11) REP(k, 2) REP(l, 2) dp[i][j][k][l] = INF; dp[0][0][0][0] = -stol(s); REP(i, n) REP(j, 1LL<<11) REP(k, 2) REP(l, 2) { ll lim = k==0 ? s[i]-'0' : 0; FOR(d, lim, 10) { ll nj = j, nk = k || d>lim, nl = l || d!=0; if(nl==1) nj |= 1LL<<d; chmin(dp[i+1][nj][nk][nl], dp[i][j][k][l] + p10[n-1-i] * d); } } REP(i, 1LL<<11) REP(j, 2) REP(k, 2) { if(__builtin_popcount(i) > x) continue; chmin(ret, dp[n][i][j][k]); } } cout << ret << endl;
ジグザグ数
「mod m」「前の桁の数」「leading-zero,leading-zeroの次の桁,増加,減少」をDPのkeyにもてばよい.leading-zeroの部分については前の桁と比べて増加も減少もしない.したがって増加/減少だけでなくleading-zeroか?をkeyにもつ必要がある.
auto solve = [&](string s) { const ll n = s.size(); REP(i, 505) REP(j, 500) REP(k, 10) REP(l, 4) dp[i][j][k][l][0] = dp[i][j][k][l][1] = 0; dp[0][0][0][0][0] = 1; REP(i, n) REP(j, m) REP(k, 10) REP(l, 4) REP(lt, 2) { if(dp[i][j][k][l][lt]==0) continue; ll lim = lt==0 ? s[i]-'0' : 9; REP(d, lim+1) { ll nj = (j*10+d) % m, nk = d, nl, nlt = lt || d<lim; if(l==0) { if(d!=0) nl = 1; else nl = 0; } else { if(k<d && l==2) continue; if(k>d && l==3) continue; if(k==d) continue; nl = k < d ? 2 : 3; } dp[i+1][nj][nk][nl][nlt] += dp[i][j][k][l][lt]; } } mint ret = 0; REP(i, 10) REP(j, 4) REP(k, 2) ret += dp[n][0][i][j][k]; return ret; }; for(ll i=a.size(); i>=0; --i) { if(a[i]>'0') { a[i]--; if(a.size()>2 && i==0 && a[i]=='0') a.erase(a.begin()); break; } a[i] = '9'; } cout << solve(b) - solve(a) << endl;
ABC117 D - XXOR
上から貪欲に決めていく方法でもできますが,桁DPでやってみます.
2進数であっても10進数と同様に桁DPを考えられる.桁目以下が確定したか?桁目までで可能な最大の というDPを行えばよい.
REP(i, 41) REP(j, 2) dp[i][j] = -INF; dp[0][0] = 0; REP(i, 40) REP(j, 2) { ll lim = j==0 ? !!(m&1LL<<(39-i)) : 1; REP(d, lim+1) { ll add = 0; REP(k, n) { ll ta = !!(a[k]&1LL<<(39-i)); if(d^ta) add += 1LL<<(39-i); } ll nj = j || d<lim; chmax(dp[i+1][nj], dp[i][j] + add); } } cout << max(dp[40][0], dp[40][1]) << endl;
Sum Equals Xor
数のペアなどについて数え上げるときも同様に桁DPを適用することができる.上から桁目が確定したか というDPをする.
の制約から の桁目,の桁目 である. 桁目を にすることに対応させたDPの遷移を行う.
- dp[i+1][j] += dp[i][j]*2
に対応
すでに が確定 or s[i]='1' ならば可能 - dp[i+1][j || s[i]=='1'] += dp[i][j]
に対応
dp[0][0] = 1; REP(i, n) REP(j, 2) { // (aのi桁目, bのi桁目) が (0,1) (1,0) if(j == 1 || s[i]=='1') dp[i+1][j] += dp[i][j] * 2; // (0,0) dp[i+1][j || s[i]=='1'] += dp[i][j]; } cout << dp[n][0] + dp[n][1] << endl;
yukicoder No.685 Logical Operations
and, xor, orの値がどのようになっているかを以下に示す.
and | xor | or | ||
---|---|---|---|---|
0 | 0 | 0 | 0 | 0 |
0 | 1 | 0 | 1 | 1 |
1 | 0 | 0 | 1 | 1 |
1 | 1 | 1 | 0 | 1 |
上の桁から見ていって のいずれかが現れた後に が現れれば条件を満たすことがわかる.よって「これらのペアが現れたか?」をkeyとすればよい.
を満たすためには「 が確定したか?」「 が確定したか?」をkeyとすればよい.
桁目各ペアが現れたか? が確定したか?が確定したか? というDP をすればよい.
mint dp[65][3][2][2]; int main(void) { ll m; cin >> m; string s = ""; while(m > 0) { s += m%2 + '0'; m /= 2; } reverse(ALL(s)); const ll n = s.size(); // [i桁目][ペアが現れたか][x<=yが確定したか][y<=mが確定したか] dp[0][0][0][0] = 1; REP(i, n) REP(j, 3) REP(k, 2) REP(l, 2) { ll lim = l || s[i]=='1'; // (0,0) ll nj = j, nk = k, nl = lim; dp[i+1][nj][nk][nl] += dp[i][j][k][l]; // (0,1) if(lim==1) { nj = j==0 ? 1 : j, nk = 1, nl = l; dp[i+1][nj][nk][nl] += dp[i][j][k][l]; } // (1,0) if(k==1) { nj = j==0 ? 1 : j, nk = k, nl = lim; dp[i+1][nj][nk][nl] += dp[i][j][k][l]; } // (1,1) if(j>=1 && lim==1) { nj = j==1 ? 2 : j, nk = k, nl = l; dp[i+1][nj][nk][nl] += dp[i][j][k][l]; } } mint ret = 0; REP(i, 2) REP(j, 2) ret += dp[n][2][i][j]; cout << ret << endl; return 0; }
ABC138 F - Coincidence
と が一致する と の最上位bitが一致 かつ の桁目,の桁目 が存在しない と言い換えられる.
桁目が確定が確定がすでに現れたか としてDPをすればよい.
dp[0][0][0][0] = 1; REP(i, 60) REP(j, 2) REP(k, 2) REP(l, 2) { ll tl = !!(L&1LL<<(59-i)), tr = !!(R&1LL<<(59-i)); // (1,1) y<=R のみできる if(k==1||tr==1) { ll nj = j || tl<1, nk = k || 1<tr; dp[i+1][nj][nk][1] += dp[i][j][k][l]; } // (0,0) L<=x のみできる if(j==1||tl==0) { ll nj = j || tl<0, nk = k || 0<tr; dp[i+1][nj][nk][l] += dp[i][j][k][l]; } // (0,1) L<=x && y<=R && l=1 のみできる if((j==1||tl==0) && (k==1||tr==1) && l==1) { ll nj = j || tl<0, nk = k || 1<tr; dp[i+1][nj][nk][l] += dp[i][j][k][l]; } } mint ret = 0; REP(i, 2) REP(j, 2) REP(k, 2) ret += dp[60][i][j][k]; cout << ret << endl;
ARC066 D - Xor Sum
桁目について が のパターンを取ることができるのか?を考える.
とすればよい
とすればよい
で下の桁で繰り上がりがある
で下の桁で繰り上がりがある && 上の桁に繰り上がる
の条件と下の桁で繰り上がりがあるか?を管理できればよい.桁目が確定が確定下の桁で繰り上がりが起きるのが確定 可能なパターン数 というDPをすればよい.
dp[0][0][0][0] = 1; REP(i, 60) REP(j, 2) REP(k, 2) REP(l, 2) { ll d = !!(n&1LL<<(59-i)); // (0,0) dp[i+1][j||0<d][k||0<d][0] += dp[i][j][k][l]; // (1,1) if((j==1||d==1) && (k==1||d==1) && l==0) { dp[i+1][j||1<d][k||1<d][0] += dp[i][j][k][l]; } // (1,0) if((j==1||d==1) && l==1) { dp[i+1][j||1<d][k||0<d][1] += dp[i][j][k][l]; } // (0,1) if(k==1||d==1) { dp[i+1][j||0<d][k||1<d][1] += dp[i][j][k][l]; } } mint ret = 0; REP(i, 2) REP(j, 2) ret += dp[60][i][j][0]; cout << ret << endl;
レシート
繰り下がりがあるか?をDPのkeyに持つ.下の桁から見ていくと繰り下がりがあったかどうかを持ちながらDPするのが楽.
東京工業大学プログラミングコンテスト2015 F - レシート - ferinの競プロ帳
Manthan, Codefest 17 Salazar Slytherin's Locket
「奇数回現れた数」をDPのkeyに持ち, 桁奇数回現れた数の集合leading-zeroか以下が確定 というDPテーブルを埋めていくことで各クエリに対して答えを求めることができる.しかし,このDPテーブルの要素数は であり, 回この桁DPを行うのはTLEしてしまう.
そこで各クエリにおいて重複して計算している部分を減らしたい.「以下が確定した」場合,クエリの 等によってDPの遷移が変化することはなくなり,クエリが異なる場合でも求めるべき値が一致する.進法残り桁奇数回現れた集合はleading-zeroか としたDPテーブルを持ちメモ化再帰することで,各クエリで計算する必要がある量を減らすことができる.
提出
最初に書いたのとだいぶ違う実装になったけど