解决的问题
一般给定一个区间 l ∼ r l\sim r l∼r,算出所有满足某一条件的数的数量,一般可以解决 1 0 1000 10^{1000} 101000级别的问题,如果暴力枚举,那么必定会超时,我们使用数位dp解决问题。
思想
- 不用传统的枚举每一位数在check一下,我们可以枚举每一位数可以的取值,用类似dfs搜索的方式枚举,然而如果纯用dfs枚举每一位数,那么最后时间复杂度不会改变,仍然要和暴力枚举一样枚举所有的数。
- 这个时候到dp上场了,dp本质就是枚举一个范围,保存一个状态,上面的搜索我们其实可以看到存在非常多的无用枚举例如:123456我们枚举111xxxx时需要继续枚举1000个数,而我们枚举112xxxx时其实数是完全一样的,保存一个dp数组可直接赋值,无需在枚举,当然,这个例子的条件是无题目条件。对于题目条件的需求,我们直接加入一维到dp数组,那么就可以完成枚举了。一般每多一个条件便在dp上加一维,之后模版上在详细聊。
解题方法
方法前置
计算 l ∼ r l\sim r l∼r,那么我们可以写出一个函数计算 0 ∼ r 0\sim r 0∼r,用类似前缀和的思想,可将结果变为 s o l v e ( r ) − s o l v e ( l − 1 ) solve(r) - solve(l - 1) solve(r)−solve(l−1),即可得到答案。注意:当给定的数过长,最初始只能用字符串存储时,只需 s o l v e ( r ) − s o l v e ( l ) solve(r) - solve(l ) solve(r)−solve(l),最后单独判断一下 l l l是否满足条件即可。
具体方法
一般分为递推法以及记忆化搜索法,记忆化搜索更容易使用。
①递推法
可简单通过图示了解,最后的答案为绿色框内所有的和,从最高位开始枚举到最后一位为止。用树的形式分类枚举位数,x为任意数,现在待解决的问题变成了如何通过递推求出每一个前面画出的绿色范围,此时就变成了传统的dp,关于递推的过程在此就不细嗦了,反正一般也不用这方法,接下来我们主要还是使用记忆化搜索的方式解决问题。
②记忆化搜索
记忆化搜索最大的优点就是不用找如何递推,暴力的美学,通过暴力搜索来实现dp,其实基本思想方法与递推差不多,不过记忆化搜索有一套基本的模版,只要按照模版来,这类题基本都能解决。那么我们还是来看模版吧。
注意题目范围大于int范围时将模版内int改成long long
- 这就是一套标准的main函数内容,就不过多介绍了,在题目条件不变的情况下,多组数据时不需要重新初始化dp数组,因为dp数组内的数值也不会变。
int dp[pos][state];
int le, ri;
int main(){
memset(dp, -1, sizeof dp); //所有dp值皆为未赋值状态
cin >> le >> ri; //左右范围
cout << solve(ri) - solve(le - 1) << endl;
}
- 接下来是solve函数中的内容,主要是初始化数据,将数据的每一位数都放入一个数组中。
int solve(int x){
int pos = 0;
while(x){
a[pos ++] = x % 10; //当题目为非10进制时,将10变为进制数即可
x /= 10;
}
return dfs(pos - 1, true, true);
}
- 重头戏来了,dfs函数中的内容(注意/* */中可加代码,//为注释)
//题目中有几个限制条件就可以一直加state
int dfs(int pos/*位数*/, int state/*题目所给条件限制*/, int lead/*前导0*/, int limit/*是否为上界*/){
/*这里可以剪枝,也可在接下来循环中剪枝*/
//当然也可以不剪枝,参数中加入个judge //if (pos == -1 && !judge) return 0; 也是可以的
if (pos == -1) return 1;//枚举到最后一位,表明该数字合法,返回1
//由递推的图可知,当前面的值固定满足题目所有状态相同,那么后面的值数量也是固定的
//当然条件是没有前导0以及不是上界限制的数
if (!lead && !limit && dp[pos][state] != -1) return dp[pos][state];
int up = limit ? a[pos] : 9;//前面一位为上界时,该位只能到a[pos],非上界可以到9
int ans = 0; //前面数固定,接着枚举所有数中的满足条件所有数量
for (int i = 0; i <= up; i ++){ //开始枚举下一位数,将每个枚举的数加入ans中
if (...) //根据题目来
ans += dfs(pos - 1, state, lead && i == 0, limit && i == a[pos]);
}
//填入dp[pos][state]的值
if (!lead && !limit && dp[pos][state] == -1) dp[pos][state] = ans;
return ans;
}
例题
通过例题来演示一下:
HDU2089-不要62
题意:给定的 l ∼ r l\sim r l∼r不出现连续的62同时不出现4的数个数。
#include <bits/stdc++.h>
using namespace std;
//dp为[pos][pre],保存了前一位数,由于出现4时直接剪枝可以不用加状态
int dp[12][12], a[12];
int dfs(int pos, int pre, int lead, int limit){
if (pos == -1) return 1;
if (!lead && !limit && dp[pos][pre] != -1) return dp[pos][pre];
int up = limit ? a[pos] : 9;
int ans = 0;
for (int i = 0; i <= up; i ++){
//本体与模版不同的地方就在下面两行,就是将所有出现4以及连续62的接下来的枝条全部减去
//那么当枚举到pos == -1时一定是合法的,加上即可
if (i == 4) continue;
if (pre == 6 && i == 2) continue;
ans += dfs(pos - 1, i, lead && i == 0, limit && i == a[pos]);
}
if (!lead && !limit && dp[pos][pre] == -1) dp[pos][pre] = ans;
return ans;
}
int solve(int x){
int pos = 0;
while (x){
a[pos ++] = x % 10;
x /= 10;
}
return dfs(pos - 1, 10, true, true);
}
int main()
{
int le, ri;
memset(dp, -1, sizeof dp);
while (cin >> le >> ri && le + ri){
cout << solve(ri) - solve(le - 1) << endl;
}
return 0;
}
基础的过了,来道稍微难点的题把
P4124手机号码(luogu)
题意:给定11位数的l和r,不能同时出现8和4,必须要有三个连号,问个数?
思路:由于必定11位,可以不需要前导0,第一位直接从1位开始枚举即可,接下来就是模版啦,传递参数bool a8,a4代表是否出现8或4,在保存前1位,前2位,pre1, pre2,参数即可。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll a[14];
ll dp[13][2][2][11][11][2]; //每一个传递的参数都设置一个状态233,挺长的
ll dfs(int pos, bool a8, bool a4, int pre1, int pre2, bool judge, int limit){
//由于要遍历完整个11位才能够判断是否合法,加入一个judge参数表示是否有三连数
if (a8 && a4) return 0;
if (pos == -1 && judge == true) return 1;
if (pos == -1 && judge == false) return 0;
int up = limit ? a[pos] : 9;
if (!limit && dp[pos][a8][a4][pre1][pre2][judge] != -1) return dp[pos][a8][a4][pre1][pre2][judge];
ll ans = 0;
//当枚举第一位时1~up,其他时0~up
int temp = 0;
if (pos == 10) temp = 1;
for (int i = temp; i <= up; i ++){
//当碰到bool类型时,可同limit类似方式,直接在dfs里判断
ans += dfs(pos - 1, a8 || i == 8, a4 || i == 4, i, pre1, judge || (pre1 == pre2 && i == pre2), limit && i == a[pos]);
}
if (!limit && dp[pos][a8][a4][pre1][pre2][judge] == -1) dp[pos][a8][a4][pre1][pre2][judge] = ans;
return ans;
}
ll solve(ll x){
int pos = 0;
while (x){
a[pos ++] = x % 10;
x /= 10;
}
ll ans = 0;
ans += dfs(pos - 1, false, false, 10, 10, false, true);
return ans;
}
int main()
{
ll le, ri;
cin >> le >> ri;
memset(dp, -1, sizeof dp);
ll res;
//由于我们的枚举是到aim->1e10,左边界为1e10时要特判
if (le == 1e10){
res = solve(ri);
}
else{
res = solve(ri) - solve(le - 1);
}
cout << res << endl;
return 0;
}
状压状态
CF855E Salazar Slytherin’s Locket
题意:l, r 转成b进制时,0~b-1出现的次数都为偶数次的数有多少个?
思路:乍一看需要b个判断参数,不好设dp和dfs的参数列表,但转念一想,不就是状压吗,每个数只有两种状态,奇数或者偶数,全是2进制,压缩成一个数,只要最后结果为0即可,每次一个数改变只要异或一个1 << 当前数值即可,可得出解法。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
ll dp[11][66][1 << 12], a[66];
ll b, le, ri;
ll dfs(int pos, int now, int lead, int limit){
if (pos == -1 && now == 0) return 1;
if (pos == -1) return 0;
if (!lead && !limit && dp[b][pos][now] != -1) return dp[b][pos][now];
int up = limit ? a[pos] : b - 1; //b进制最高位b - 1
ll ans = 0;
for (int i = 0; i <= up; i ++){
if (lead && i == 0) //去除前导0,防止对0的个数造成影响
ans += dfs(pos - 1, now, lead && i == 0, limit && i == a[pos]);
else
ans += dfs(pos - 1, now ^ (1 << i), lead && i == 0, limit && i == a[pos]);
}
if (!lead && !limit && dp[b][pos][now] == -1) dp[b][pos][now] = ans;
return ans;
}
ll solve(ll x){
int pos = 0;
while (x){
a[pos ++] = x % b;
x /= b;
}
ll ans = 0;
ans = dfs(pos - 1, 0, true, true);
return ans;
}
int main()
{
int t;
cin >> t;
memset(dp, -1, sizeof dp);
while (t --){
cin >> b >> le >> ri;
cout << solve(ri) - solve(le - 1) << endl;
}
return 0;
}
看到这里了,你一定会数位dp了吧(bushi