桁 DP のややこしい点は、遷移の上限を場合分けしなければいけない点だと思います。具体的には、以下のような分岐を辿ります。(その桁の上限の数を $A_i$ とします)
- 余裕がある → $0\sim9$ まで遷移可能
- 余裕がない → $0\sim A_i$ まで遷移可能
- 次の遷移は $A_i$ 未満か? → 余裕ができる
- 次の遷移は $A_i$ か? → 余裕がないまま
 
なので、コードの可読性を重視すると以下のような感じになると思います。
説明のためのシンプルな例として、「$N$ 以下の数を桁DPで求める」というコードを書きます。
   vector<vector<ll>> dp(71,vector<ll>(2));
   dp[0][0] = 1;
   rep(i, 70)
   {
       rep(next, 10)
       {
           dp[i + 1][1] += dp[i][1];
       }
       rep(next, a[i] + 1)
       {
           if (next < a[i])
           {
               dp[i + 1][1] += dp[i][0];
           }
           else
           {
               dp[i + 1][0] += dp[i][0];
           }
       }
   }
しかし、できれば、
    rep(...) // 何らかのループ
    {
        rep(...) // 何らかのループ
        {
            // 何らかの一行の処理
        }
    }
という形で書けた方が助かる。なぜなら、何か変更があった時に、手を加えるところがひとつで済むからです。それに、ループの範囲自体が変わるよりも、ループは同じだけど、適用される操作によって違いが出ると考えた方が、見通しがいい。
基本形
結論として、以下のように書けます。
   rep(i,70){
       rep(small,2){
           rep(next,10){
               dp[i+1][small||(next<a[i])] += dp[i][small]*(small||(next<=a[i]));
           }
       }
   }
- 
iどの桁まで見たか
- 
small余裕があるか
 式の左に出てくる不等号が <、式の右に出てくる不等号が <= であることに注意です。
これが基本形になります。
後はこの式に付加情報を足すだけで、基本的な桁 DP を一行で解くことができます。
ABC-C 007
    rep(i, 70)
    {
        rep(contains, 2)
        {
            rep(small, 2)
            {
                rep(next, 10)
                {
                    dp[i + 1][contains || next == 4 || next == 9][small || (next < a[i])] += dp[i][contains][small] * (small || (next <= a[i]));
                }
            }
        }
    }
- 
iどの桁まで見たか
- 
contains$4$ か $9$ のいずれかを少なくともひとつ含むか
- 
small余裕があるか
ナベアツ数
みんな大好きナベアツ数も一行で遷移することができます。
    rep(i, 70)
    {
        rep(mod, 3)
        {
            rep(contains, 2)
            {
                rep(small, 2)
                {
                    rep(next, 10)
                    {
                        dp[i + 1][(mod + next) % 3][(next == 3) || contains][small || (next < a[i])] += dp[i][mod][contains][small] * (small || (next <= a[i]));
                    }
                }
            }
        }
    }
- 
iどの桁まで見たか
- 
mod$3$ で割ったあまり
- 
contains$3$ を少なくともひとつ含むか
- 
small余裕があるか
Leading Zero
桁 DP では、leading zero を単位元として 扱ってはいけない 場合は対策をする必要があります。つまり、「$321$」を「$...000321$」として扱うとバグが出てしまうような場合です。
具体的にはこういう問題など。
さて、この場合の基本的な遷移はどうなるか。
- 余裕がある → $0\sim9$ まで遷移可能
- 余裕がない → $0\sim A_i$ まで遷移可能
- 次の遷移は $A_i$ 未満か? → 余裕ができる
- 次の遷移は $0$ か? → leading zero 継続
- 次の遷移は $0$ 超過か? → leading zero 離脱
 
- 次の遷移は $A_i$ か? → 余裕がないまま
- 次の遷移は $0$ か? → leading zero 継続
- 次の遷移は $0$ 超過か? → leading zero 離脱
 
 
- 次の遷移は $A_i$ 未満か? → 余裕ができる
こうなります。皆様はすんなり理解できますか? 私はできません。
 結論から言うと、「leading zero から遷移する数を始めから計上しておく」ことが対策になります。通常の桁 dp だと、dp[0]...[0] = 1 という「種」から増殖していくのが、先に dp の各表にそういう 1 が増えるので、個人的な感覚ですが「複数の種を先に撒いておく」というイメージがあります。
つまり、dp テーブルに対して、先に以下のような種を撒いておきます。
    rep(i,m){
        if(i==0){
            for(int j=1;j<=a[0];j++){ // 1 始まりである点に注意
                dp[1][j<a[0]]++;    
            }
        }else{
            for(int j=1;j<=9;j++){
                dp[i+1][1]++;
            }
        }
    }
これは、以下のような一行遷移でまとめることができます。
    rep(i,m){
        for(int j=1;j<10;j++){
            dp[i+1][i||(j<a[0])] += i||(j<=a[0]);
        }
    }
実装例
チートシート
せっかくなのでチートシートを公開します。リーディングゼロ対応です。決め打ちにぶたんで小さい方から $K$ 番目の数を答える形にしています。初期状態だとただ自分の数を数えるだけですが、各自改造してください。
long long solve() {
    long long k;
    std::cin >> k;
    long long ng = 0;
    long long ok = 1e18; // 10^18
    // バイナリサーチで条件を満たす最小の値を探索
    while (abs(ng - ok) > 1) {
        long long mid = (ng + ok) / 2;
        // midを桁ごとに分解してvectorに格納
        std::vector<long long> a(20);
        long long copy = mid;
        for (int i = 0; i < 20; i++) {
            a[i] = copy % 10;
            copy /= 10;
        }
        // 末尾の0を削除
        while (!a.empty() && a.back() == 0) {
            a.pop_back();
        }
        // 桁を逆順に(自然な数値の並びにする)
        std::reverse(a.begin(), a.end());
        int m = a.size();
        // DPテーブルを定義
        using VI = std::vector<long long>;
        using VVI = std::vector<VI>;
        using VVVI = std::vector<VVI>;
        VVVI dp(m + 1, VVI(10, VI(2, 0))); // dp[桁][現在の数][smallerフラグ]
        // 初期条件:1桁目
        for (int i = 0; i < m; i++) {
            for (int j = 1; j < 10; j++) {
                dp[i + 1][j][i || (j < a[0])] += i || (j <= a[0]);
            }
            // 遷移
            for (int now = 0; now < 10; now++) {
                for (int small = 0; small < 2; small++) {
                    for (int next = 0; next < 10; next++) {
                        dp[i + 1][next][small || (next < a[i])] += 
                            dp[i][now][small] * (small || next <= a[i]);
                    }
                }
            }
        }
        // 合計を計算
        long long tmp = 0;
        for (int j = 0; j < 10; j++) {
            for (int small = 0; small < 2; small++) {
                tmp += dp[m][j][small];
            }
        }
        // バイナリサーチの更新
        if (tmp >= k) {
            ok = mid;
        } else {
            ng = mid;
        }
    }
    return ok;
}
ll solve(){
    ll k;
    cin >> k;
    ll ng = 0;
    ll ok = 1e18;
    while(abs(ng-ok)>1){
        ll mid = ng+ok;
        mid /= 2;
        vector<ll> a(20);
        ll copy = mid;
        rep(i,20){
            a[i] = copy%10;
            copy /= 10;
        }
        while(a.back()==0){
            a.pop_back();
        }
        reverse(all(a));
        int m = a.size();
        using VI = vector<ll>;
        using VVI = vector<VI>;
        using VVVI = vector<VVI>;
        using VVVVI = vector<VVVI>;
        using VVVVVI = vector<VVVVI>;
        VVVI dp(m+1,VVI(10,VI(2)));
        rep(i,m){
            for(int j=1;j<10;j++){
                dp[i+1][j][i||(j<a[0])] += i||(j<=a[0]);
            }
            rep(now,10){
                rep(small,2){
                    rep(next,10){
                        dp[i+1][next][small||(next<a[i])] += dp[i][now][small]*(small||next<=a[i]);
                    }
                }
            }
        }
        ll tmp = 0;
        rep(j,10){
            rep(small,2){
                tmp += dp[m][j][small];
            }
        }
        if(tmp>=k){
            ok = mid;
        }else{
            ng = mid;
        }
    }
    return ok;
}