DP实战:数位DP和伪三维DP
例题引入
我们定义以下正整数变形操作。
对于一个正整数 x,如果其二进制表示恰好包含 y 个 1,那么经过一次变形操作后,x 将变为 y。
例如,对于正整数 13,其二进制表示为
1101
,其中恰好包含 3 个 1,所以 13 经过一次变形操作后会变为 3。如果一个正整数通过上述变形操作变为 1 所需要的最少操作次数恰好为 k,那么就称这个数是一个易变数。
给定一个正整数 n 的不含前导 0 的二进制表示,请你计算 [1,n] 范围内的易变数的数量。
由于结果可能很大,你只需要输出对 109+7 取模后的结果。
输入格式
第一行包含正整数 n 的不含前导 0 的二进制表示。
第二行包含非负整数 k。
输出格式
一个整数,表示 [1,n] 范围内的易变数的数量对 10^9+7 取模后的结果。
数据范围
前 5 个测试点满足 1≤n<2^{10}。
所有测试点满足 1≤n<2^{1000},0≤k≤1000。输入样例1:
110 2
输出样例1:
3
输入样例2:
111111011 2
输出样例2:
169
题目大意:
有一个f映射,输入x,输出y。y为x二进制表示
中1的个数。
定义易变数_k:x经过最少k次f映射得到1
从1到N中,找到易变数_k的个数。
个数过大则对 10^9+7 取模
题目分析:
这是一个非常经典的搜索题。
1.先从f入手(规律或者限制),发现并不能从数学角度寻求规律,那我们看看这个f的作用效果是什么?显然当f作用到x上时,x的值被限定到1~1000之间。
显然,我们只要用哈希表储存1到1000的所对应的k值即可。
输入n,k我们即可计算得到NUM,每一个满足题意的数的二进制数位中1的数量和为NUM
2.突然发现最少值这一限制,所谓最少k其实就是1的f本身就是1,发么就无关紧要了。
3.遍历所有的数有于1到2^1000这个数据范围是非常大的,单纯的依次搜索肯定是不可以,关键在于1的数量。由于n的不确定性,最开始的想法是对n退一位从而找到1的最大数量。
但是观察发现,我们对每一位进行0和1的选择即可,即***找到1到n的二进制表示中有NUM个1的数的数量
DFS暴力
定义dfs:
dfs(
i:第i位
,k:k个1
) //除此之外,结合改题目,我们还要加入两个辅助变量用来控制dfs的停止
定义return:
return 1:
当i遍历到第0位时,且k为target时停止,cnt++;
return 2:
遍历的当前数大于n,或k大于target时,return
递归与回溯:
递归:选择
0
或者1
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define M (ll)(1e9+7)
ll dp[1001][1001][4];
int cnt = 0;
int num[1001];
//获得1到1000,对应的k值
int F(int x)
{
if (x == 1)
return 0;
bitset<15> a(x);
int i = a.count();
return F(i) + 1;
}
void dfs(int i, int k,int target,int state)
{
if (k == target && i== 0 &&state)
{
cnt++;
cnt = cnt % M;
return;
}
if (k > target|| !state ||i==0)return;
//大小不确定
if (state == 1)
{
if (num[i-1] == 1) {
//选0
dfs(i-1, k, target, 2);
//选1;
dfs(i - 1, k + 1, target, 1);
}
else {
//选0
dfs(i - 1, k, target, 1);
//选1
dfs(i - 1, k + 1, target, 0);
}
}
//已经确定大小
else {
dfs(i - 1, k, target, state);
dfs(i - 1, k + 1, target, state);
}
}
int main()
{
unordered_map<int, vector<int> >a;
for (int i = 1; i <= 1000; i++)
a[F(i)].push_back(i) ;
string t; cin >> t;
//字符串转化为数组储存
for (int i=t.size()-1;i>=0; i--)
num[t.size()-i-1] = t[i] - '0';
int k; cin >> k;
//提取出所要寻找的1的数量
vector<int> res = a[k-1];
//用dfs搜索
for (auto x : res)
dfs(t.size(), 0, x,1);
cout << cnt << endl;
}
伪三维DP(由DFS演变)
dfs(i,k,state)---->DP[i,k,state]
代码如下:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define Mod (ll)(1e9+7)
ll dp[1001][1001][4];
ll cnt = 0;
ll num[1001];
//获得1到1000,对应的k值
ll F(ll x)
{
if (x == 1)
return 0;
bitset<15> a(x);
ll i = a.count();
return F(i) + 1;
}
int main()
{
unordered_map<ll, vector<ll> >a;
for (ll i = 1; i <= 1000; i++)
a[F(i)].push_back(i);
string t; cin >> t;
//字符串转化为数组储存
for (ll i = t.size() - 1; i >= 0; i--)
num[t.size() - i - 1] = t[i] - '0';
ll k; cin >> k;
if (t.size() == 1&&k)
{
cout << 0 << endl;
return 0;
}
//提取出所要寻找的1的数量
vector<ll> res = a[k - 1];
//用dp
memset(dp, 0, sizeof(dp));
int t0 = 0; int t1 = 0;
for (int i = 1; i <=t.size(); i++)
{
if (num[i - 1])
t1++;
else
t0++;
dp[i][t1][0] = 1;
}
//判断第一位
//为0时
if (num[0] == 0)
dp[1][1][2] = 1;
//为1时
if (num[0] == 1)
dp[1][0][1] = 1;
//开始dp递归
for (int i = 2; i <= t.size(); i++)
for (int j = 0; j <= i; j++) {
if (num[i - 1] == 1)
{ //当该位实际为1时
//如果选择0,则之前的数都比n小
for (int k = 0; k <= 2; k++)
dp[i][j][1] += dp[i - 1][j][k];
//如果选择1,则之前比n小的继续小于n
dp[i][j][1] += dp[i - 1][j - 1][1];
//如果选择1,则之前比n大的继续大于n
dp[i][j][2] = dp[i - 1][j - 1][2];
}
else
{
//当该位实际为0时
//如果选择1,则之前的数都比n大
for (int k = 0; k <= 2; k++)
dp[i][j][2] += dp[i - 1][j - 1][k];
//如果选择0,则之前比n小的继续小于n
dp[i][j][1] = dp[i - 1][j][1];
//如果选择0,则之前比n小的继续大于n
dp[i][j][2] += dp[i - 1][j][2];
}
//取模
for (int k = 0; k <= 2; k++)
{
dp[i][j][k] = dp[i][j][k] % Mod;
}
}
for (auto x : res)
{
if (x <= t.size()) {
cnt += dp[t.size()][x][1];
cnt += dp[t.size()][x][0];
}
}
cout << k << endl;
if (k)
cout << cnt << endl;
else
cout << 1 << endl;
return 0;
}
数位DP(从ACwing中转载)
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 1010, mod = 1e9 + 7;
char str[N];
int m; //易变数的最少操作次数
int g[N], f[N][N]; //g[]来表示要操作多少次才能变成1
int main()
{
scanf("%s%d", str, &m);
int n = strlen(str);//求出二进制位数
if(m == 0) puts("1"); //特判1
else if(m == 1) printf("%d\n", n - 1);//特判10,100,1000...
else
{
//因为n最多为1000位,全是1的话操作一次变为1000,本题1000即是最大的值了
for(int i = 2; i <= 1000; i ++) //打表预处理1000以内的情况
{
int cnt = 0; //求出二进制表示共有几个1
for(int j = i; j >= 1; j >>= 1)
{
if(j & 1) cnt ++;
}
g[i] = g[cnt] + 1;
}
for(int i = 1; i <= n; i ++) f[0][i] = g[i] == m - 1; //判断是否可以在m次内完成
for(int i = 1; i <= n; i ++)
{
for(int j = 0 ; j <= n; j ++)
{
f[i][j] += f[i - 1][j];
if(j + 1 <= n) f[i][j] = (f[i][j] + f[i - 1][j + 1]) % mod;
}
}
int res = 0, t = 0; //t用于下面求出当前位置i前面有几个1
for(int i = 0; i < n; i ++) //数位dp,开始求分支,求答案
{
if(str[i] == '1')
{
res = (res + f[n - i - 1][t]) % mod;
//当前位置:i,从i + 1 ~ n - 1位一共有 n - i - 1个数,前面t个1
t ++;
}
}
if(g[t] == m - 1) //特判右边边界上的最后一个数,它操作之后会变成t
res = (res + 1) % mod;
printf("%d\n", res);
}
return 0;
}
作者:雾隂
链接:https://www.acwing.com/solution/content/206932/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。