SRM 372 500pt: RoundOfEleven
問題概要
10桁以下の整数が与えられる。各桁の数字を1増やすか減らすのに1単位のコストがかかる。コストの総和が一定量以下となるようにして11の倍数をたくさん作りたい。このとき、Σ(初期コスト-必要なコスト)を求める問題。
考えたこと
- 10進数で11の倍数かどうかは偶数桁の和と奇数桁の和の差が11の倍数かどうかで判定できる。これは10≡-1 (mod 11)であることから簡単に示される。
- なので偶数桁と奇数桁に分けて考えればよさげ。でもちょっと面倒くさいか?
- 偶数桁だけ集めたら、各桁の合計値も必要なコストも高々50以下なのでどうとでもなりそう。
- 一番素朴にやろうとしたら、dp[i番目まで見た][合計値][必要なコスト]=そういう場合の数、としてDPで行けそう。
- 書いた。投げた。通らない。
- 最後に偶数桁と奇数桁をマージするとき必要なコストの上限が50のままだった。二つの和だから100くらいにしなきゃダメだった。
- 無事通った。
practiceで通ったコード
#include <cstring> #include <vector> #include <algorithm> using namespace std; typedef long long int64; const int MAX_N = 55; const int MAX_LOG = 7; int result[2][MAX_LOG][MAX_N][MAX_N]; //(偶奇、合計値、総コスト) struct RoundOfEleven { int64 maxIncome(int n, int money){ //10未満のときは大丈夫? //偶数桁の数字と奇数桁の数字に分割する vector<int> odd, even; for(int i=0; n > 0; i++, n/=10){ if(i&1){ odd.push_back(n%10); } else{ even.push_back(n%10); } } //偶奇でそれぞれ何通りできるか計算する func(odd, 0); func(even, 1); int64 ret = 0; for(int i=0; i<50; i++)for(int j=0; j<50; j++)if((i-j)%11 == 0){ //kは使用した金額の合計 for(int k=0; k<min(money, 100); k++){ int64 income = money - k; for(int a=0; a<=k; a++)if(a<=50 && k-a<=50){ ret += income * result[0][odd.size()][i][a]*result[1][even.size()][j][k-a]; } } } return ret; } void func(const vector<int>& nums, const int sign){ const int N = nums.size(); int (*dp)[MAX_N][MAX_N] = result[sign]; dp[0][0][0] = 1; for(int i=0; i<N; i++){ for(int cur=0; cur<50; cur++){ //合計値の上限は5*9=45 for(int cost=0; cost<50; cost++)if(dp[i][cur][cost]!=0){ //総コストの上限も5*9=45 for(int k=0; k<=9; k++){ //iつめの数字を何に置き換えるか int add = abs(nums[i] - k); if(cur+k < 50 && cost + add < 50){ dp[i+1][cur+k][cost+add] += dp[i][cur][cost]; } } } } } } };
別の解き方1
DPとかしなくても偶数桁(奇数桁)だけ取り出したら作れる数はたかだか10^5しかないのだから全て列挙できる。この方法は半分全列挙と言われるアレ。ただし今回の場合は計算量は落ちる。
void func2(const vector<int>& nums, int array[MAX_N][MAX_N]){ const int N = nums.size(); //つくることのできる数の上界を求める int max_d = 1; for(int i=0; i<N; i++){ max_d *= 10; } //つくることのできる数を総当たりで試す for(int i=0; i<max_d; i++){ //桁に現れる数を集める vector<int> digs; for(int d=i, k=0; k<N; k++, d/=10){ digs.push_back(d%10); } //その数を作るためのコストとかを求める int cost = 0, sum = accumulate(digs.begin(), digs.end(), 0); for(int k=0; k<N; k++){ cost += abs(digs[k]-nums[k]); } array[sum][cost]++; } }
別の解き方2
基本的には最初に書いたコードと大差ない。DPで、偶数と奇数に分ける必要はなく、奇数桁のときはマイナスの符号をつけてやれば良い。
あと、実装上の工夫として、配列の一部をポインタで置き換えるようなことをしてindexに負の数突っ込めるようにしたけど、このときうっかりmemsetでの初期化を間違えてしまった。これは注意しないとハマりそう。
int dp[2][102][102]; int64 maxIncome(int n, int money){ //桁に現れる数字を集める vector<int> nums; for(int m=n; m; m/=10){ nums.push_back(m%10); } const int N = nums.size(); //配列を使いまわしてDP int (*cur)[102] = dp[0]+51, (*next)[102] = dp[1]+51; cur[0][0] = 1; for(int i=0; i<N; i++){ for(int sum=-45; sum<=45; sum++){ for(int cost=0; cost<=100; cost++)if(cur[sum][cost] > 0){ for(int k=0; k<=9; k++){ int add_cost = abs(nums[i] - k); int add_num = ((i&1)?k:-k); next[sum+add_num][cost+add_cost] += cur[sum][cost]; } } } swap(cur,next); memset(next-51, 0, sizeof(dp[0])); //初期化の仕方に注意!! } int64 ret = 0; for(int sum=-45; sum<=45; sum++)if(sum%11 == 0){ for(int cost=0; cost<= min(100, money); cost++){ ret += (int64)(money - cost) * cur[sum][cost]; } } return ret; } };