数位dp是有模版和套路的,
AcWing 1081. 度的数量
这题我觉得最好结合辰风的题解去看,而且有个bug这题 预处理组合数数组和input交换位置 代码计算的结果不同。 很玄学
#include <iostream>
#include <vector>
const int N = 31 + 5;
int f[N][N]; //从a个数里面选b个1有多少种方法
int l, r, k, b;
int dp(int n)
{
if (n == 0) return 0;
std::vector<int> nums;
while (n) nums.push_back(n % b), n /= b;//转化为b进制并存储下来每一位
int last = 0, res = 0;
for (int i = nums.size() - 1; i >= 0; -- i)//存的时候是逆序的
{
int x = nums[i];
if (x > 0)
{
//当前位取0
res += f[i][k - last];//先把当前位取0的情况加上(这里i的原因是i是下标,正好是真实的位置-1,正好可以用来表示当前还剩多少位)
//当前位取1
if (x > 1)//当前位取1的时候要看取得是这个位的上限还是 0 ~ 上限 - 1,如果x > 1显然是后者
{
if (k - last - 1 >= 0) res += f[i][k - last - 1];
break;//取1的时候如果不是上限,后面几位直接求一下组合数就行了,肯定不会超过n的b进制表示的数的大小
}
else if (x == 1)//说明当前位取1的时候取的是当前位的上限
{
//取的是上限,还需要继续讨论
last ++ ;
if (last > k) break;
}
}
if (i == 0 && last == k) res ++;//如果到了最后一位的时候恰好1选够k个了,那么最后一位选择0也是合法的,最后一位不取0的情况在上面x>0中讨论过了
}
return res;
}
int main()
{
//这题 预处理组合数数组和input交换位置 代码计算的结果不同。 很玄学
//预处理组合数数组
for (int i = 0; i <= N; ++ i)
for (int j = 0; j <= i; ++ j)
{
if (j == 0) f[i][j] = 1;
else f[i][j] = f[i - 1][j] + f[i - 1][j - 1];
}
//input
std::cin >> l >> r >> k >> b;
//dp
std::cout << dp(r) - dp(l - 1);
return 0;
}
AcWing 1082. 数字游戏
#include <iostream>
#include <vector>
using namespace std;
const int N = 15;//2^31转换成10进制最多10 ^ 10
int f[N][10];//计算一共i位数且最高位是j的 非降序数方案数有多少
int l, r;
void init()
{
for (int i = 0; i <= 9; ++ i) f[1][i] = 1;//当前只有1位的时候,方案数是1
for (int i = 2; i <= N - 1; ++ i)//数组最大就开了N位
for (int j = 0; j <= 9; ++ j)
for (int k = j; k <= 9; ++ k)
f[i][j] += f[i - 1][k];
}
int dp(int n)
{
if (n == 0) return 1;//如果n=0,nums.size() - 1返回值就是无穷大,因为nums.size()返回值是无符号形的
int last = 0, res = 0;
vector<int> nums;
while(n) nums.push_back(n % 10), n /= 10;
for (int i = nums.size() - 1; i >= 0; -- i)
{
int x = nums[i];
if (x < last) break;
for (int j = last; j < x; ++ j)//枚举0~当前位的上限值 - 1,即左分支
res += f[i + 1][j];//序号下标从0开始的,i + 1是加上当前正在枚举的这一位一共有i + 1个位,度的数量那题是因为要计算去掉当前位剩下多少位中含j个1的组合数有多少
last = x;
if (i == 0) res ++ ;//枚举完所有位后也算一种方案,因为f[1][i] = 1,当前只有一个位的时候无论这个位上是什么,非降序方案数都是1
}
return res;
}
int main()
{
init();
while (cin >> l >> r) cout << dp(r) - dp(l - 1) << endl;
return 0;
}
AcWing 1083. Windy数
这题挺难的,和之前的思路大致一样,但是细节上有很多不同点
// //枚举nums.size位的数字的时候记得最高位从1开始,因为有前导0的情况我们会在 小于nums.size位的数字中加上,不然会加多了,
// //关于前导零,13 符合 windy 数,但 013 却不符合,因为 abs(0 - 1) < 2
#include <iostream>
#include <cmath>
#include <vector>
using namespace std;
const int N = 15;
int f[N][10];//考虑当前一共有i位且最高位为j的时候满足windy数定义的数一共有多少
int l, r;
void init()
{
for (int i = 0; i <= 9; ++ i) f[1][i] = 1;
for (int i = 2; i <= N - 1; ++ i)
for (int j = 0; j <= 9; ++ j)
for (int k = 0; k <= 9; ++ k)
if (abs(k - j) >= 2) f[i][j] += f[i - 1][k];
}
int dp(int n)
{
if (n == 0) return 0;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int last = -2, res = 0;
//枚举位数位nums.size()的windy数
for (int i = nums.size() - 1; i >= 0; -- i)
{
int x = nums[i];
for (int j = (i == nums.size() - 1); j < x; ++ j)//当前位不能超过当前位的上限,且如果当前位是最高位的话必须从1开始枚举,这点题目中说了
if (abs(j - last) >= 2)
res += f[i + 1][j];
if (abs(x - last) < 2) break;//本题中这个判断不能放在最前面了,因为我们j是从0/1开始枚举的,而不是从x - 1开始
last = x;//从这里就可以看出树形dp实际上是将整个树枚举一遍,
//上面那个for是将左分支的方案收集起来,
//这里直接定义我们当前位选的就是当前位的上限,再通过最外层的for进入下一位的左分支讨论方案数
if (i == 0) res ++; //说实话,到这我还是不知道为什么所有位讨论完后还要res++
}
//枚举位数位nums.size()的windy数,位数比n少一位,那么肯定当前数不会比n大,直接枚举我们之前预处理过的f[i][j]即可
for (int i = 1; i <= nums.size() - 1; ++ i)
for (int j = 1; j <= 9; ++ j)//因为不含前导0,为了保证第i位有效,我们j不能从0开始
res += f[i][j];
return res;
}
int main()
{
init();
cin >> l >> r;
cout << dp(r) - dp(l - 1) ;
return 0;
}
AcWing 1084. 数字游戏 II
我只能说模运算博大精深。。。
#include <iostream>
#include <vector>
#include <cstring>
using namespace std;
const int N = 10 + 5;
int f[N][10][102];//当前一共有i位,且最高位为j的数所有位上的和模p余数为k 的 数一共有多少
int l, r, p;
int mod(int n, int p)
{
return (n % p + p) % p;//c++的mod结果可以是负数,因此要加一个偏移量p,再%p的原因是如果mod的结果是正数,这个%就有用了
}
void init()
{
memset(f, 0, sizeof f);
for (int j = 0; j <= 9; ++ j) f[1][j][j % p] ++;
for (int i = 2; i <= N - 1; ++ i)
for (int j = 0; j <= 9; ++ j)
for (int k = 0; k < p; ++ k)
for (int x = 0; x <= 9; ++ x)
f[i][j][k] += f[i - 1][x][mod(k - j, p)];//这遍历很有讲究,首先便利ijk三维我们定义好的状态,
//i位肯定是从i-1位状态转移过来的,因此我们还需要枚举i-1位数的最大位是哪些数,即x
//具体余数是多少呢,我们在i-1位的数上少了i位数的最大位的j,
//因此我们i-1位的数sum应该是k-j,为了保证我们k-j是一个正数且合法(第三维的状态应该在p以内)
//我们用mod(k - j, p)保证
}
int dp(int n)
{
if (n == 0) return 1;
vector<int> nums;
while (n) nums.push_back(n % 10), n /= 10;
int last = 0, res = 0;
for (int i = nums.size() - 1; i >= 0; -- i)
{
int x = nums[i];
//处于左分支 位上取的数在0~当前上限-1
for (int j = 0; j < x; ++ j)//处于左分支,当前数一定不大于整个数的上限,直接用我们预处理好的f即可
{
res += f[i + 1][j][mod(-last, p)];//从0~i一共有i+1位,我们要保证这i+1位的数的和sum且(sum+last)%p == 0;
//前面几位的和是last,因此我们要使得我们的sum % p = -last,
//因为-last越界了不合法,第三维得在0~p - 1之间,所以我们需要mod(-last, p),
//使f[i+1][j][-last]变成合法的f[i+1][j][0~p-1]也就是f[i + 1][j][mod(-last, p)]
}
//处于右分枝,即当前位确定为当前位的上限,我们直接通过for遍历下一位即可(右分枝可以分解为下一位的左分支和右分枝)
last += x;
//如果所有位都确定了(取的都是每一位上限,即一直通过右分枝走到底,我们判断这个数的每一位之和符不符合题意)
if (i == 0 && last % p == 0) res ++;
}
return res ;
}
int main()
{
while (cin >> l >> r >> p) init(), cout << dp(r) - dp(l - 1) << endl;
return 0;
}
AcWing 1085. 不要62
这题不知道为啥我用vector实现就不行
#include <iostream>
#include <vector>
using namespace std;
const int N = 10;
int f[N][10];//一个共i位的数,最高位是j的时候合法的车牌号有多少
int l, r;
void init()
{
for (int j = 0; j <= 9; ++ j) f[1][j] = 1;
f[1][4] = 0;
for (int i = 2; i <= N - 1; ++ i)
for (int j = 0; j <= 9; ++ j)
for (int k = 0; k <= 9; ++ k)
{
if (j == 4 || k == 4) continue;
if (j == 6 && k == 2) continue;
f[i][j] += f[i - 1][k];
}
}
int dp(int x)
{
if(!x) return 1; //日常边界判断,有的数位DP题里必须有,这道题需不需要你交一下试试就知道了
int a[12] = {0}; //我要用a数组把这个数的每一位都取出来
int l = 0;
int ans = 0;
int last = 0; //last用来看上一位是不是6,如果是6 那么这一位我就不能填2了
while(x)
{
a[++ l] = x % 10;
x /= 10;
}
for(int i = l;i >= 1;i --)
{
for(int j = 0;j < a[i];j ++) //因为是车牌,所以第一位可以是0,故从0循环
{
if(j == 4) continue;
if(last == 6 && j == 2) continue;
ans += f[i][j];
}
if(a[i] == 4) break;
if(a[i] == 2 && last == 6) break;
last = a[i];
if(i == 1) ans ++; //本来没加这句话,但是写完发现答案少一,于是加上,表示所有的都填完了,传进来的这个x也能填
}
return ans;
}
// int dp(int n)
// {
// if (n == 0) return 1;
// vector<int> nums;
// while (n) nums.push_back(n % 10), n /= 10;
// int last = 0, res = 0;//last表示上一位是什么数
// for (int i = nums.size() - 1; i >= 0; -- i)
// {
// int x = nums[i];
// //枚举左分支,选择的数字j 在 0 ~ 当前位的上限 - 1
// for (int j = 0; j < x; ++ j)
// {
// if (j == 4) continue;
// if (last == 6 && j == 2) continue;
// res += f[i][j];
// }
// //枚举右分枝,右分枝就是选择当前位的上限
// if (x == 4) break;
// if (last == 6 && x == 2) break;
// last = x;
// if (i == 0) res ++;//枚举到最后一位,没有下一位的左分支可以进入了,已经到头了,即每一位都选择的上限,并且经过前3句的判断合法,那么也算一种方案
// }
// return res;
// }
int main()
{
init();
while(cin >> l >> r,l && r)
{
cout << dp(r) - dp(l - 1) << endl;
}
return 0;
}```