DP实战:数位DP和伪三维DP

DP实战:数位DP和伪三维DP

例题引入

5271. 易变数 - AcWing题库

我们定义以下正整数变形操作。

对于一个正整数 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]

dfs:t到0中二进制表示NUM个1的个数
与dfs相同
表示为一共有i个数,一共有j个1,与n相比为k的数的个数

代码如下:

#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
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
  • 5
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值