「REMAKE系列」数位dp篇

模板及技巧总结

数位DP

常前缀和思想来解决 \([l,r]\) 范围内满足 xxx 要求的数字个数。

经典数位DP

较难写的数位DP

记搜模板

以求满足 存在 3 个连续数位单调递增的数字个数为例

ll dp[20][2][10][5];

ll dfs(int rem, int exist, int last, int inc) {     // (剩余,存在满足要求的数,上一位数,当前递增个数)
    ll& ans = dp[rem][exist][last][inc];
    if (ans != -1) return ans;
    if (!rem) return exist;
    ans = 0;
    for (int i = 0; i <= 9; i ++) {
        int new_inc = (i > last) ? min(3, inc + 1) : 1;
        ans += dfs(rem - 1, exist | (new_inc == 3), i, new_inc);
    }
    return ans;
}

ll solve(ll x) {
    x ++;       // ++ 之后方便处理
    vector<int> d;      // 存每一位
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));    // 反转
    int m = d.size();
    ll ans = 0;
    // 处理前导零
    for (int i = 1; i <= m - 1; i ++) {
        for (int j = 1; j <= 9; j++)
            ans += dfs(i - 1, 0, j, 1);     // 第 i 位确定,还剩 i - 1 位。
    }
    int rem = 0, exist = 0, inc = 0, last = 0;      // 记录前面固定的数位的状态
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {     // 如果是最高位从 1 开始枚举
            int new_inc = (j > last) ? min(3, inc + 1) : 1;     // 更新下一次搜索的状态,和 dfs 里的其实很像
            ans += dfs(m - i - 1, exist | (new_inc == 3), j, new_inc);
        }
        // 结束一轮枚举,更新固定数位的状态。
        // 注意如果前面固定的数位已经不满足条件了,就需要 break;
        inc = (d[i] > last) ? min(3, inc + 1) : 1;
        last = d[i];
        exist |= (inc == 3);
    }
    return ans;
}

计数

排列计数

习题

dls动态规划中级课

数数3

dls数位DP中级课

  • 求满足 存在 3 个连续数位单调递增的数字个数
ll dp[20][2][10][5];

ll dfs(int rem, int exist, int last, int inc) {
    ll& ans = dp[rem][exist][last][inc];
    if (ans != -1) return ans;
    if (!rem) return exist;
    ans = 0;
    for (int i = 0; i <= 9; i ++) {
        int new_inc = (i > last) ? min(3, inc + 1) : 1;
        ans += dfs(rem - 1, exist | (new_inc == 3), i, new_inc);
    }
    return ans;
}

ll solve(ll x) {
    x ++;
    vector<int> d;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size();
    ll ans = 0;
    // 处理前导零
    for (int i = 1; i <= m - 1; i ++) {
        for (int j = 1; j <= 9; j++)
            ans += dfs(i - 1, 0, j, 1);
    }
    int rem = 0, exist = 0, inc = 0, last = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
            int new_inc = (j > last) ? min(3, inc + 1) : 1;
            ans += dfs(m - i - 1, exist | (new_inc == 3), j, new_inc);
        }
        inc = (d[i] > last) ? min(3, inc + 1) : 1;
        last = d[i];
        exist |= (inc == 3);
    }
    return ans;
}

CF1560F2. Nearest Beautiful Number (hard version)

Codeforces Round #739 (Div. 3)

  • 定义 k-美丽数 为数位中不同数位最多为 \(k\) 的数字。
  • 求大于等于 \(n\) 的最小 k-美丽数。

这类问题核心:快速判断剩下的位是否能满足要求(new_cnt <= k)

vector<int> d;
int n, k;
int vis[11];        // 多组数据清空 vis[],直接返回dfs不一定回溯
bool dfs(int x, bool larger, int cnt, int num) {
    if (x == (int)d.size()) {
        printf("%d\n", num);
        return true;
    }
    else {
        for (int i = larger ? 0 : d[x]; i <= 9; i ++) {
            vis[i]++;
            int new_cnt = cnt;
            if (vis[i] == 1) new_cnt++;
            if (new_cnt <= k && dfs(x + 1, larger | (i > d[x]), new_cnt, num * 10 + i))
                return true;
            vis[i]--;
        }
    }
    return false;
}

void solve(int x) {
    d.clear();
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    dfs(0, 0, 0, 0);
}

不同位大于等于 \(k\) 最小数字求法

这里的核心代码就是 new_cnt + (int)d.size() - x - 1 >= k ,还要减 1 是因为当前位的贡献已经计算在了 new_cnt 里面。

vector<int> d;
int n, k;
int vis[11];

bool dfs(int x, bool larger, int cnt, int num) {
    if (x == (int)d.size()) {
        printf("%d\n", num);
        return true;
    }
    else {
        for (int i = larger ? 0 : d[x]; i <= 9; i ++) {
            vis[i]++;
            int new_cnt = cnt;
            if (vis[i] == 1) new_cnt++;
            if (new_cnt + (int)d.size() - x - 1 >= k && dfs(x + 1, larger | (i > d[x]), new_cnt, num * 10 + i))
                return true;
            vis[i]--;
        }
    }
    return false;
}

void solve(int x) {
    d.clear();
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    while (1) {
        if (dfs(0, 0, 0, 0)) break;     // 当前位数搜不到就要扩大位数,否则 break
        int m = d.size() + 1;
        d.clear();
        d.push_back(1);     // 原来有 m 位,扩大为 m + 1 位,那么从最小的 1000000开始,有 m 个 0
        for (int i = 0; i < m - 1; i ++) d.push_back(0);         
    }
}

数位背包入门——梦幻岛宝珠

P3188 [HNOI2007]梦幻岛宝珠

思路

  • 形如背包重量为 \(a * 2^x\) 的题目,可以用数位背包的方法来尝试解题
  • 由高到低位考虑,\(f[i][j]\) 表示到第 \(i\)还剩 \(j\)\(2^i\) 容量的最大价值。
  • 由于重量的系数 和 物品个数限制,考虑到第 \(i\) 位时,后面的所有位加起来贡献到第 \(i\) 位的重量是有限的,所以直接和物品重量总数取 min
  • 然后注意细节和转移,由于状态定义为还剩xxx时的最大价值,所以要用 剩的多反而背包价值更大 去更新剩的少的价值,代码中是后缀 max
const int N = 2010;
ll f[N], g[N];
int n, W;
// f[i][j] 到第 i 位,还剩 j 个容量的最大价值
vector<PII> item[35];
void solve() {
    for (int i = 0; i <= 31; i ++) item[i].clear();
    int sum = 0;    // 重量总数
    for (int i = 0; i < n; i ++) {
        int w, v;
        re(w), re(v);
        int lev = __builtin_ctz(w);
        w >>= lev;
        item[lev].pb({w, v});
        sum += w;
    }
    for (int i = 0; i <= sum; i++) f[i] = -2e18;
    f[0] = 0;
    for (int i = 30; i >=0 ; i--) {
        for (int j = 0; j <= sum; j++) g[j] = f[j], f[j] = -2e18; // 滚动数组
        int d = W >> i & 1;
        for (int j = 0; j <= sum; j ++) {       // 不选物品,i->i+1 背包状态转移,容量 * 2 + d
            f[min(sum, 2 * j + d)] = max(f[min(sum, 2 * j + d)], g[j]);
        }
        // 选物品,背包转移, f[i][j] = max(f[i][j], f[i - 1][j + w] + v);
        // w <= j <= sum, 所以需要记录 f[i][j] 为后缀 f[][] 最大值, 因为剩的更多价值更大,剩的少的价值应该被更新掉。
        for (int j = sum - 1; j >= 0; j--)
            f[j] = max(f[j], f[j + 1]);
        for (auto p: item[i]) {         
            for (int j = 0; j <= sum - p.first; j++) {
                f[j] = max(f[j], f[j + p.first] + p.second);
            }
        }
    }
    printf("%lld\n", f[0]);
}

2020-CCPC长春-D, Meaningless Sequence

  • 有个小结论然后无脑做,有个二项式定理,\((1+c)^k = \Sigma_i^k(c^i * \binom{k}{i})\)
const int N = 3010, mod = 1e9 + 7;
ll power[N];

int main() {
    // freopen("input.txt", "r", stdin);
    // freopen("output.txt", "w", stdout);
    string s;
    cin >> s;
    int c, n = s.size();
    re(c);
    power[0] = 1;
    for (int i = 1; i <= N - 10; i++)
        power[i] = power[i - 1] * (c + 1) % mod;
    ll pre = 1, ans = 0;
    for (int i = 0; i < n; i ++) {
        if (s[i] == '0') continue;
        ans = (ans + pre * power[n - i - 1] % mod) % mod;
        pre = pre * c % mod;
    }
    ans += pre;
    printf("%lld\n", ans % mod);
    // fclose(stdin);
    // fclose(stdout);
    return 0;
}

2020-ICPC济南-L. Bit Sequence

通过 (61/545)智慧暴力

思路

  • 从二进制高位向低位进行 dp,因为 \(m\) 较小,只会影响低 7 位。
  • 考虑搜索到第 8 位时,最终的结果只和 3 个东西有关。
    • 低 7 位的状态
    • 连着第 8 位的连续 1 的个数
    • 第 8 位以后的 1 的奇偶性。
  • 预处理一下低 7 位对应有效状态个数,小于等于 7 位搜索时,可以直接暴力做。
  • 放上自己写的屎山代码和网上题解代码。
#include<bits/stdc++.h>
#define pb push_back
#define ALL(x) x.begin(), x.end()
#define popcount(x) (__builtin_popcountll(x) % 2)
#define SZ(x) ((int)x.size())
using namespace std;
typedef long long ll;
const int N = 110;
int a[N], m;
ll L;
int A, B, C, D, E, F, G, H;	// E all yes, F all no

int _popcount(int x) {
	int cnt = 0;
	for (int i = 7; i >= 0; i--) {
		cnt += x >> i & 1;
	}
	cnt &= 1;
	return cnt;
}
void init() {
	int lim = 255;
	A = B = C = D = E = F = G = H = 0;
    for (int i = 0; i <= lim; i++) {
        bool same = _popcount(i) == a[0];
        bool ok = 1;
        for (int j = 0; j < m; j++) {
            if ((a[j] == _popcount(i + j)) != same) ok = 0;
        }
        if (ok) 
            same ? G++ : H++;
    }
	for (int i = lim - m + 2; i <= lim; i++) {
		bool same = popcount(i) == a[0], changed = 0;
		bool ok = 1;
		for (int j = 1; j < m && ok; j++) {
			int t = _popcount(i + j);
			if (t == a[j] && !same) {
				if (!changed) {
                    if (i + j != lim + 1) ok = 0;
                    changed = 1, same ^= 1;
                }
				else ok = false;
			}
			else if (t != a[j] && same) {
				if (!changed) {
                    if (i + j != lim + 1) ok = 0;
                    changed = 1, same ^= 1;
                }
				else ok = false;
			}
		}
		if (ok && changed) {
			if (popcount(i) == a[0]) A ++;
			else B++;
		}
	}
	for (int i = 0; i < lim - m + 2; i++) {
		bool ok = 1, same = popcount(i) == a[0];
		for (int j = 1; j < m && ok; j++) {
			if ((popcount(i + j) == a[j]) != same) ok = 0;
		}
		if (ok) 
			same ? C ++ : D ++;
	}
	for (int i = 0; i <= 255; i++) {
		bool ok = 1;
		for (int j = 0; j < m && ok; j++) {
			int x = i + j;
			if (popcount(x) != a[j]) ok = 0;
		}
		E += ok;
	}
	for (int i = 0; i <= 255; i++) {
		bool ok = 1;
		for (int j = 0; j < m && ok; j++) {
			int x = i + j;
			if (popcount(x) == a[j]) ok = 0;
		}
		F += ok;
	}
}

ll dp[63][2][63];

ll dfs(int rem, bool cnt, int inc) {
	ll & ans = dp[rem][cnt][inc];
	if (ans != -1) return ans;
	ans = 0;
	if (rem != 9) {
		ans += dfs(rem - 1, cnt, 0);
		ans += dfs(rem - 1, cnt ^ 1, inc + 1);
	}
	else {
		// fill 1
		bool new_cnt = cnt ^ 1;
        int new_inc = inc + 1;
		if (new_inc & 1) {
			if (!new_cnt) ans += G;
			else ans += H;
		}
		else if (new_inc % 2 == 0) {
			if (!new_cnt) ans += A + C;
			else ans += B + D;
		}
		// fill 0
		if (!cnt)
			ans += E;
		else 
			ans += F;
	}
	return ans;
}

int DFS(int rem, ll val) {
	if (!rem) {
		for (int i = 0; i < m; i++) {
			ll x = val + i;
			if (popcount(x) != a[i]) return 0;
		}
		return 1;
	}
	int ans = 0;
	ans += DFS(rem - 1, val);
	ans += DFS(rem - 1, val + (1ll << (rem - 1)));
	return ans;
}

ll solve() {
	vector<int> d;
	int head = 0;
	for (int i = M; i >= 0; i--) {
		if (L >> i & 1) {
			head = i;
			break;
		} 
	}
	for (int i = head; i >= 0; i--) {
		d.pb(L >> i & 1);
	}
	int len = SZ(d);
	ll ans = 0;
	ll tot = 0;
	for (int i = 1; i < len; i++) {
		if (i <= 9)
			ans += DFS(i - 1, 1ll << (i - 1));
		else 
			ans += dfs(i - 1, 1, 1);
	}
	bool cnt = 0;
	int inc = 0;
	ll val = 0;
	for (int i = 0; i < len; i++) {
		for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
			int rem = len - i - 1;
			if (rem >= 9) {
				ans += dfs(rem, cnt, 0);
			}
			else if (rem == 8) {
				// fill 0
				if (!cnt)
					ans += E;
				else 
					ans += F;
			}
			else {
				ans += DFS(rem, val);
			}
		}
		cnt ^= d[i];
		d[i] ? inc ++ : inc = 0;
		val |= ((1ll * d[i]) << (len - i - 1));
	}
	bool ok = 1;
	for (int i = 0; i < m; i++) {
		if (popcount(L + i) != a[i]) ok = 0;
	}
	ans += ok;
	if (L) {
		ok = 1;
		for (int i = 0; i < m; i++) {
			if (popcount(i) != a[i]) ok = 0;
		}
		ans += ok;
	}
	return ans;
}

int main() {
	int T;
	scanf("%d", &T);
	while (T--) {
		memset(dp, -1, sizeof dp);
		scanf("%d%lld", &m, &L);
		for (int i = 0; i < m; i++)
			scanf("%d", &a[i]);
		init();
		printf("%lld\n", solve());
	}
	return 0;
}

洛谷——「能力综合提升题单-数位计数DP」, Codeforces

P4999 烦人的数学作业

普及+/提高

\([L,R]\) 数字的数位和

  • 数位dp模板题
const int mod = 1e9 + 7;
int dp[20][200];        // 不能是 dp[rem][last]
int dfs(int rem, int sum) {     
    if (!rem) return sum;
    if (dp[rem][sum] != -1) {
        return dp[rem][sum];
    }
    int& ans = dp[rem][sum];
    ans = 0;
    for (int i = 0; i <= 9; i++) 
        ans = (ans + dfs(rem - 1, sum + i)) % mod;
    return ans;
}
ll solve(ll x) {
    vector<int> d;
    x++;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size();
    ll ans = 0;
    for (int i = 0; i <= m - 2; i ++) {
        for (int j = 1; j <= 9; j++)
            ans = (ans + dfs(i, j)) % mod;
    }
    int sum = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
            ans = (ans + dfs(m - i - 1, sum + j)) % mod;
        }
        sum += d[i];
    }
    return ans;
}
int main() {
    int T;
    re(T);
    memset(dp, -1, sizeof dp);
    while (T--) {
        ll l, r;
        re(l), re(r);
        printf("%lld\n", (solve(r) - solve(l - 1) + mod) % mod);
    }
    return 0;
}

P2606 [ZJOI2010]排列计数

提高+/省选-二叉树小根堆、lucas定理、计数dp

int main() {
    int n;
    re(n), re(p);
    get_fact();
    for (int i = 1; i <= n; i++) s[i] = 1;
    for (int i = n; i >= 2; i--) s[i >> 1] += s[i];
    // C_{i - 1}^l * f[l] * f[r];
    for (int i = 1; i <= n * 2 + 1; i++) dp[i] = 1;
    for (int i = n; i >= 1; i--) {
        dp[i] = lucas(s[i] - 1, s[2 * i], p) * dp[2 * i] % p * dp[2 * i + 1] % p;
    }
    printf("%lld\n", dp[1]);
    return 0;
}

P2657 [SCOI2009] windy 数

提高+/省选-数位dp

  • 搜索状态:\(dp[rem][last]\) ,剩余多少位以及上一位是多少
int dp[12][10];

int dfs(int rem, int last) {
    int& ans = dp[rem][last];
    if (rem == 0) return ans = 1;
    if (ans != -1) return ans;
    ans = 0;
    for (int i = 0; i <= 9; i++) {
        if (abs(i - last) < 2) continue;
        ans += dfs(rem - 1, i);
    }
    return ans;
}

int solve(int x) {
    x ++;
    vector<int> d;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size(), ans = 0;
    for (int i = 1; i <= m - 1; i++) {
        for (int j = 1; j <= 9; j++) 
            ans += dfs(i - 1, j);
    }
    int last = -2;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0) ? 1 : 0; j < d[i]; j++) {
            if (abs(j - last) < 2) continue;
            ans += dfs(m - i - 1, j);
        }
        if (abs(d[i] - last) < 2) break;    // 已经不合法就不能再搜了
        last = d[i];
    }
    return ans;
}

P4124 [CQOI2016]手机号码

省选/NOI-数位dp模板

出现连续 \(3\) 个相同数且不同时出现 \(4,8\) 的 11 位数。

// rem, last, exist, state inc
ll dp[13][11][2][5][5];

int cal(int num, int s) {
    if (num == 4) s |= 1;
    else if (num == 8) s |= 2;
    return s;
}

ll dfs(int rem, int last, bool exist, int state, int inc) {
    if (!rem) return exist;
    ll& ans = dp[rem][last][exist][state][inc];
    if (ans != -1) return ans;
    ans = 0;
    if (rem + inc < 3 && !exist) return ans;
    for (int i = 0; i <= 9; i++) {
        int new_state = cal(i, state);
        int new_inc = i == last ? min(3, inc + 1) : 1;
        int new_exist = exist | (new_inc == 3);
        if (new_state == 3) continue;
        ans += dfs(rem - 1, i, new_exist, new_state, new_inc);
    }
    return ans ;
}

ll solve(ll x) {
    x++;
    vector<int> d;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size();
    ll ans = 0;
    int last = -1, exist = 0, state = 0, inc = 0;
    for (int i = 0; i < m; i++) {
        if (state == 3) break;
        for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
            int new_state = cal(j, state);
            if (new_state == 3) continue;
            int new_inc = (j == last ? min(3, inc + 1) : 1);
            int new_exist = exist | (new_inc == 3);
            ans += dfs(m - i - 1, j, new_exist, new_state, new_inc);
        }
        inc = d[i] == last ? min(inc + 1, 3) : 1;
        last = d[i];
        state = cal(last, state);
        exist |= (inc == 3);
    }
    return ans;
}

int main() {
    ll l, r;
    re(l), re(r);
    memset(dp, -1, sizeof dp);
    printf("%lld\n", solve(r) - solve(l - 1));
    return 0;
}

CF628D Magic Numbers

省选+/NOI-简单数位dp

int d, M;
// rem, R, 
ll dp[2010][2010];
string l, r;
const int mod = 1e9 + 7;

ll dfs(int rem, int R, bool is_even) {
    ll& ans = dp[rem][R];
    if (ans != -1) return ans;
    if (!rem) return ans = R == 0;
    ans = 0;
    is_even ^= 1;
    for (int i = 0; i <= 9; i++) {
        if (is_even && i != d) continue;
        if (!is_even && i == d) continue;
        int new_R = (R * 10 + i) % M;
        ans += dfs(rem - 1, new_R, is_even);
        ans %= mod;
    }
    return ans;
}


bool check(string s) {
    int t = 0;
    for (int i = 0; i < SZ(s); i++) {
        if (i & 1 && (s[i] - '0') != d) return 0;
        if (i % 2 == 0 && (s[i] - '0') == d) return 0;
        t = (t * 10 + (s[i] - '0')) % M;
    }
    return t == 0;
}
ll solve(string s) {
    vector<int> v;
    for (int i = 0; i < SZ(s); i++) v.pb(s[i] - '0');
    int m = v.size();
    ll ans = 0;
    int R = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0 ? 1 : 0); j < v[i]; j++) {
            int new_R = (R * 10 + j) % M;
            if ((i % 2 == 0 && (j == d)) || (i & 1 && j != d)) continue;
            ans += dfs(m - i - 1, new_R, i & 1);
            ans %= mod;
        }
        if ((i % 2 == 0 && (v[i] == d)) || (i & 1 && v[i] != d)) break;
        R = (R * 10 + v[i]) % M;
    }
    if (check(s)) ans++;
    return ans;
}
int main() {
    IOS;
    string l, r;
    memset(dp, -1, sizeof dp);
    cin >> M >> d;
    cin >> l >> r;
    ll ans = (solve(r) - solve(l) + check(l) + mod) % mod;
    cout << ans << endl;
    return 0;
}

P3413 SAC#1 - 萌数

省选+/NOI-数位dp、结论

回文的存在条件:存在某一位和上一位或上两位相同

const int mod = 1e9 + 7;
// rem, last1, last2, exist
ll dp[1010][13][13][2];
bool check(string s) {
    int last1 = 11, last2 = 11;
    for (int i = 0; i < SZ(s); i++) {
        int n = s[i] - '0';
        if (n == last1 || n == last2) return 1;
        last2 = last1, last1 = n;
    }
    return 0;
}
ll dfs(int rem, int last1, int last2, bool exist) {
    ll& ans = dp[rem][last1][last2][exist];
    if (ans != -1) return ans;
    if (!rem) return ans = exist;
    ans = 0;
    for (int i = 0; i <= 9; i++) {
        bool new_exist = exist | (i == last1 || i == last2);
        ans += dfs(rem - 1, i, last1, new_exist);
        ans %= mod;
    }
    return ans;
}
ll solve(string s) {
    vector<int> d;
    for (int i = 0; i < SZ(s); i++)
        d.pb(s[i] - '0');
    int m = SZ(d);
    ll ans = 0;
    for (int i = 1; i < m; i++) {
        for (int j = 1; j <= 9; j++) {
            ans += dfs(i - 1, j, 11, 0);
            ans %= mod;
        }
    }
    int last1 = 11, last2 = 11;
    bool exist = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
            bool new_exist = exist | (j == last1 || j == last2);
            ans += dfs(m - i - 1, j, last1, new_exist);
            ans %= mod;
        }
        exist = exist | (d[i] == last1 || d[i] == last2);
        last2 = last1;
        last1 = d[i];
    }
    ans += check(s);
    ans %= mod;
    return ans;
}
int main() {
    // IOS;
    string l, r;
    cin >> l >> r;
    memset(dp, -1, sizeof dp);
    ll ans = solve(r) - solve(l) + check(l);
    ans = (ans + mod) % mod;
    cout << ans << endl;
    return 0;
}

P4127 [AHOI2009]同类分布

省选+/NOI-数位dp

求出 \([a,b]\) 中各位数字之和能整除原数的数的个数。

  • 因为原数是不确定的,但原数和有范围 \(\leq 9\times len\),最多 \(180\)
  • 所以枚举所有可能的原数和(模数),复杂度是够的。
const int M = 200;
// rem, R,
int mod;
ll dp[20][M + 2][M + 2];
vector<int> d;
ll dfs(int rem, int sum, int R) {
    ll& ans = dp[rem][sum][R];
    if (ans != -1) return ans;
    if (!rem) return ans = (sum == mod && (R == 0));
    ans = 0;
    for (int i = 0; i <= 9; i++) {
        int new_sum = sum + i;
        int new_R = (R * 10 + i) % mod;
        if (new_sum > mod) break;
        ans += dfs(rem - 1, new_sum, new_R);
    }
    return ans;
}
ll solve2() {
    ll ans = 0;
    int m = d.size();
    for (int i = 1; i < m; i++) {
        for (int j = 1; j <= min(9, mod); j++) {
            ans += dfs(i - 1, j, j % mod);
        }
    }
    int sum = 0, R = 0;
    ll val = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
            int new_R = (R * 10 + j) % mod;
            ans += dfs(m - i - 1, sum + j, new_R);
        }
        sum += d[i];
        R = (R * 10 + d[i]) % mod;
        if (sum > mod) break;
    }
    return ans;
}
ll solve1(ll x) {
    d.clear();
    x ++;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    ll ans = 0;
    for (mod = 1; mod <= SZ(d) * 9; mod ++) {
        memset(dp, -1, sizeof dp);
        ans += solve2();
    }
    return ans;
}
int main() {
    ll l, r;
    cin >> l >> r;
    printf("%lld\n", solve1(r) - solve1(l - 1));
    return 0;
}

CF1073E. Segment Sum

*2300

给定 \(K(K\leq 10),L,R\) ,求 \([L,R]\) 之间最多不包含超过 \(K\) 个数码的数的和。

思路

  • 考虑按位贡献来计算。
  • 设状态为 \(dp[rem][state]\) ,剩余 rem 位,已填数数码状态为 state 的和。
  • 由于后续还可以对应多个数,所以还需要一个数组记录剩余 rem 位,已填数码状态为 state 的数字个数,故以下转移:
    • \(dp_{rem,state} = \sum_j dp_{rem-1,new_state} + g_{rem-1,new_state} \times j \times 10^{rem-1}\)
  • 实现细节:在最后一个 for 循环时,由于原位的数字并没有被计入贡献,所以需要记录每个数位后续有多少个数,因为是从高位到低位循环,而个数是从低位开始算,所以需要再循环一次。
const int mod = 998244353;
int K;
// rem, state;
ll dp[20][1030], g[20][1030];
ll mi[22], ki[20];

void add(ll& a, ll b) {
    b %= mod;
    a += b;
    if (a >= mod) a -= mod;
}

ll dfs(int rem, int state) {
    ll& ans = g[rem][state];
    if (ans != -1) return ans;
    if (!rem) {
        dp[rem][state] = 0;
        return ans = __builtin_popcount(state) <= K;
    }
    ans = 0;
    dp[rem][state] = 0;
    for (int i = 0; i <= 9; i++) {
        int new_state = state | (1 << i);
        dfs(rem - 1, new_state);
        add(ans, g[rem - 1][new_state]);
        add(dp[rem][state], dp[rem - 1][new_state] + mi[rem - 1] * i % mod * g[rem - 1][new_state] % mod);
    }
    return ans;
}

ll solve(ll x) {
    ll t = x;
    x ++;
    vector<int> d;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size();
    ll ans = 0;
    for (int i = 1; i < m; i++) {
        for (int j = 1; j <= 9; j++) {
            dfs(i - 1, 1 << j);
            add(ans, dp[i - 1][1 << j] + mi[i - 1] * j % mod * g[i - 1][1 << j] % mod);
        }
    }
    ll val = 0, state = 0;
    vector<int> v;
    ll sum = 0;
    for (int i = 0; i < m; i++) {
        ll g_cnt = 0;
        for (int j = (i == 0 ? 1 : 0); j < d[i]; j ++) {
            int new_state = state | (1 << j);
            dfs(m - i - 1, new_state);
            add(ans, dp[m - i - 1][new_state] + mi[m - i - 1] * j % mod * g[m - i - 1][new_state] % mod);
            add(g_cnt, g[m - i - 1][new_state]);
        }
        state |= 1 << d[i];
        if (i)
            v.pb(g_cnt), add(sum, g_cnt);
    }
    for (int i = 0; i < SZ(v); i++) {
        add(ans, d[i] * mi[m - i - 1] % mod * sum % mod);
        sum = (sum - v[i] + mod) % mod;
    }
    return ans;
}

int main() {
    ll l, r;
    re(l), re(r);
    re(K);
    mi[0] = 1, ki[0] = 1;
    for (int i = 1; i <= 19; i++) {
        mi[i] = mi[i - 1] * 10 % mod;
    }
    memset(dp, -1, sizeof dp);
    memset(g, -1, sizeof g);
    printf("%lld\n", (solve(r) - solve(l - 1) + mod) % mod);
    return 0;
}

CF55D Beautiful numbers

*2500数位dp、数论

一个数字 \(x\) 是美丽的,当且仅当 \(x\in\mathbb{Z^+}\) 并且对于 \(x\) 的每一个非零位上的数 \(y\),都有 \(y|x\)

思路

  • 找出 1-9 的最大公约数,最大是 \(2520\)
  • 套路式设计状态 \(dp[rem][R]\) 剩余 rem 位,模最大公约数的余数是 R。
  • 发现没法计算,到了最终的时候我们需要 所有数码的最大公约数 能整除 数值,这里与 R 等价,故加入状态中。
  • 直接开三维开不下,压缩最大公约数的因子个数,总共 \(48\) 个。
const int M = 2520;
ll dp[20][M + 1][50];
vector<int> alls;

int find(int x) {
    return lower_bound(ALL(alls), x) - alls.begin();
}
ll gcd(ll a, ll b) {
    return b ? gcd(b, a % b) : a;
}
ll _lcm(ll a, ll b) {
    return a / gcd(a, b) * b;
}
ll dfs(int rem, int R, int idx) {
    ll& ans = dp[rem][R][idx];
    if (ans != -1) return ans;
    if (!rem) return ans = R % alls[idx] == 0;
    ans = 0;
    for (int i = 0; i <= 9; i++) {
        int new_R = (R * 10 + i) % M;
        int new_idx = idx;
        if (i) 
            new_idx = find(lcm(alls[idx], i));
        ans += dfs(rem - 1, new_R, new_idx);
    }
    return ans;
}

ll solve(ll x) {
    x ++;
    vector<int> d;
    while (x) {
        d.pb(x % 10);
        x /= 10;
    }
    reverse(ALL(d));
    int m = d.size();
    ll ans = 0;
    for (int i = 1; i < m; i++) {
        for (int j = 1; j <= 9; j++) 
            ans += dfs(i - 1, j, find(j));
    }
    int R = 0, idx = 0;
    for (int i = 0; i < m; i++) {
        for (int j = (i == 0 ? 1 : 0); j < d[i]; j++) {
            int new_R = (R * 10 + j) % M;
            int new_idx = idx;
            if (j)
                new_idx = find(lcm(alls[idx], j));
            ans += dfs(m - i - 1, new_R, new_idx);
        }
        R = (R * 10 + d[i]) % M;
        if (d[i])
            idx = find(lcm(alls[idx], d[i]));
    }
    return ans;
}

int main() {
    int T;
    re(T);
    memset(dp, -1, sizeof dp);
    for (int i = 0; i < (1 << 9); i++) {
        int x = 1;
        for (int j = 0; j < 9; j++) {
            if (i >> j & 1) {
                x = lcm(x, j + 1);
            }
        }
        alls.pb(x);
    }
    sort(ALL(alls));
    alls.erase(unique(ALL(alls)), alls.end());
    while (T--) {
        ll l, r;
        re(l), re(r);
        printf("%lld\n", solve(r) - solve(l - 1));
    }
    return 0;
}

CF855E. Salazar Slytherin's Locket

*2200 进制,状态压缩

评测记录

题意

\(l...r\)之间转成\(b\)进制后,\(0,1,2...,b-2,b-1\)都出现偶数次的数的个数。

\(1\le q \le 10^5\)

\(2\le b \le 10\)

\(1\le l \le r \le 10^{18}\)

思路

  • 压缩 B 进制下 B 个数的奇偶状态进行数位dp。
  • 这个题需要将进制这一维压进去,否则每次 memset 会超时。

CF401D. Roman and Numbers

*2000状态压缩+卡常数位dp

评测记录

题意

\(n\,(n\leq 10^{18})\)的各位数字重新排列(不允许有前导零),求可以构造几个 \(\mod m\) 等于 \(0\) 的数字。

思路

  • 读完题,一股浓烈的数位 DP 的味道。那么就开始非常套路地设计状态 \(dp_{rem,R,state}\) 表示剩余 \(rem\) 位,当前余数为 \(R\) ,剩余数的状态为 \(state\) 的合法数字个数。
    • \(state\): 一个长度为 10 的 vector,下标 i 表示数字 i 剩余多少个数。
  • 状态转移:枚举能转移到哪些数字 \(j\) ,则有

\[dp_{rem,R,state} = \sum_{j=0}^9 dp_{rem-1,new\_R, new\_state} \]

  • 解释转移变量:\(new\_R=(R\times10+j)\mod m\)\(new\_state_j = state_j-1\)
  • 然后就开始愉快的卡常数和复杂度,使用 map 会卡在 test 80,使用 unordered_map 并手写对 vector 的简单哈希函数可以通过本题 (3300ms~3800ms)。另外务必注意传参vector时加入引用。

区域赛真题

2020ICPC上海 C. Sum of Log

通过 99 / 682双线程数位dp

思路

  • 先发现要求的式子,就是找 i, j 最高位的累加和。
  • 然后可以进行数位DP,先把 i,j 填充位数一样方便搜索。
  • 然后智慧数位dp。
#include<bits/stdc++.h>
#define pb push_back
#define ALL(x) x.begin(),x.end()
#define SZ(x) ((int)x.size())
typedef long long ll;
using namespace std;
const int mod = 1e9 + 7;
int X, Y;
ll dp[33][2][2][2];
vector<int> dx, dy;

const int M = 32;

ll dfs(int rem, bool lim1, bool lim2, bool top) {
    ll& ans = dp[rem][lim1][lim2][top];
    if (ans != -1) return ans;
    ans = 0;
    if (!rem) return ans = 1;
    int up1 = lim1 ? dx[M - rem] : 1, up2 = lim2 ? dy[M - rem] : 1;
    for (int i = 0; i <= up1; i++) {
        for (int j = 0; j <= up2; j++) {
            if (i & j) continue;
            ll res = 1;
            if (!top && (i || j)) res = rem;
            ans += dfs(rem - 1, lim1 && i == up1, lim2 && j == up2, top || i || j) * res % mod; // 如果前面已经有 1 了,只需要数剩余位的个数,否则乘一个权值。
            ans %= mod;
        }
    }
    return ans;
}


ll solve() {
    ll ans = 0;
    dx.clear(), dy.clear();
    for (int i = M - 1; i >= 0; i--) {
        dx.pb(X >> i & 1), dy.pb(Y >> i & 1);
    }
    return dfs(M, 1, 1, 0);
}

int main() {
    int T;
    scanf("%d", &T);
    while (T--) {
        memset(dp, -1, sizeof dp);
        scanf("%d%d", &X, &Y);
        printf("%lld\n", (solve() - 1 + mod) % mod);    // 计算了 0,0
    }
    return 0;
}

posted @ 2022-09-07 01:59  Roshin  阅读(98)  评论(0编辑  收藏  举报
-->