SRM510 Div1Easy TheAlmostLuckyNumbersDivOne(再)

http://community.topcoder.com/stat?c=problem_statement&pm=11461

昨日のskypeのあと、桁DPについてなんとなく理解したので演習用に再挑戦。実装において参考にしたのは以下のページです。(特に遷移について詳しく書かれていたのが良かったです。ありがとうございます)
torus711.hatenablog.com

今回の場合、状態は
dp[見ている桁][n以下が確定][4と7以外の個数][頭が0か否か]
の4つとします。4つ目の条件がやや特殊ですが、これがないと一部の数え上げがうまくいきません。

ex.) 12345以下について数え上げる場合
例えば、"707"は明らかにAlmostLuckyNumberです。しかしこのDPにおいて"707"は"00707"と認識されるため、4or7以外が3つ("00-0-")使われたもの、すなわちAlmostLuckyNumberでないとみなされることになります。これを回避するには、頭の"00"を4or7以外の数としてカウントしなければいいわけなので、簡単なフラグを利用します。

i:=0/1=数えたい数字の最上位が0/else、を設定して"00707"をDP風に解釈すると
"0????"(i=0)→"00???"(i=0)→"007??"(i=1)→"0070?"(i=1)→"00707"(i=1)
となっていい感じに頭の"00"を無視できます。あとはこれを書き下すだけです。


初歩的な話かもしれないですけど、多くの桁DP(いうほど見てない)でdp[0][0]…[0]=1として上手くいく理由がわかりません。直感に反するというか、dp[0][0]…[0]に対する場合の数は定義できない気がして。今回だって、0桁目を見ていてn以下が確定していなくて4と7以外の個数が0個で頭が0の数ってなんだよっていう。*1

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

class TheAlmostLuckyNumbersDivOne {
public:
    long long count(string s){
        int n=s.size();
        // 桁/確定/4.7以外個数/頭0,頭0以外
        long long dp[20][2][20][2]={};
        dp[0][0][0][0]=1;
        for(int i=1;i<=n;i++){
            int num=s[i-1]-'0';
            for(int j=0;j<2;j++){
                for(int d=0;d<=(j?9:num);d++){
                    for(int k=0;k<20;k++){
                        for(int l=0;l<2;l++){
                            if(d==0&&l==0) dp[i][j||d<num][k][l||d!=0]+=dp[i-1][j][k][l];
                            else dp[i][j||d<num][k+(d!=4&&d!=7)][l||d!=0]+=dp[i-1][j][k][l];
                        }
                    }
                }
            }
        }
        long long ret=0;
        for(int j=0;j<2;j++)for(int k=0;k<2;k++){
            ret+=dp[n][j][k][1];
        }
        return ret;
    }

    long long find( long long a, long long b ) {
       string A=to_string(a-1);
       string B=to_string(b);
       return count(B)-count(A);
    }
};

*1:追記:ここのコメント欄が答えっぽいけどモヤモヤが取れない。 桁DP入門 - pekempeyのブログ