数位dp

Cat又在颓了,果然自己在家做题的感觉就是,读个题,啊我好想去翻题解啊……也不排除是因为遇到了数位dp这种鬼畜被吓着了……

“熟抄题解三百遍,不会做题也能A”

A. Windy 数 && B. 花神的数论题

一个dfs居然比暴力快那么多,可能是因为加了个记忆化吧。

套路:把原序列存到数组里,从高位到低位枚举的过程中判断是否恰好上限,不断把标记向后传递,能存入记忆化数组里的都是不含任何限制条件的dp值,因为限制条件的情况复杂过于细节,既不容易找到好的存储方式也一般不会被利用,就干脆不记录了。

注意:不要看到很多0之后加一个7的模数就是1e9+7吧,好好数数;如果一定要用快读读入长整形数,不要只改函数类型,还要把里面的变量类型也改改。WA 10 和 TLE 30还是很有区别的,暴力分也是分啊!

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 33;
const int maxc = 1e5;

int a[30], f[30][30];//f[i][j]中i表示数位,j表示当前位的上一位上的数字

inline int read()
{
    int x = 0, f = 1;
    char ch = getchar();
    while(ch > '9' || ch < '0')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

ll dfs(int pos, int lead, int flag, int num)
{
    if(pos < 1) return 1;
    if(!flag && !lead && f[pos][num]!=-1)
    {
        return f[pos][num];
    }
    int up = flag ? a[pos] : 9;//如果高位已经更小了,低位的数就不再具有限制意义
    ll res = 0;
    for(int i=0; i<=up; i++)//枚举当前位填什么数
    {
        if(!lead && abs(num-i)<2) continue;
        res += dfs(pos-1, lead&&i==0, flag&&i==a[pos], i);
    }
    if(!flag && !lead)//为什么flag成立不能转移?
    {
        f[pos][num] = res;
    }
    return res;
}

ll solve(ll x)
{
    int k = 0;
    while(x)
    {
        a[++k] = x % 10;//设置上限
        x /= 10;
    }
    return dfs(k, 1, 1, 0);//flag=1?因为两个最高位无论什么情况下都会有限制关系
}

int main()
{
    int a1 = read(), b = read();
    memset(f, -1, sizeof(f));
    printf("%lld\n", solve(b)-solve(a1-1));

    return 0;
}
Windy数
#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 33;
const ll mod = 1e7 + 7;//默认取模1e9+7,Cat又大E了

ll n, f[55][55];
int a[55], len;

inline ll read()
{
    ll x = 0, f = 1;//改ll要改两处——暴力分都拿不全的Cat
    char ch = getchar();
    while(ch > '9' || ch < '0')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

ll dfs(int p, int st, int limit)//st是指已经填了几个1?
{
    if(p > len) return max(st, 1);
    if(f[p][st]!=-1 && !limit) return f[p][st];
    ll ret = 1;
    int res;
    if(limit) res = a[len-p+1];//两个顺序是反着的,a里先存的是最低位
    else res = 1;
    for(int i=0; i<=res; i++)
    {
        (ret*=dfs(p+1, i==1?st+1:st, limit&&(i==res)))%=mod;
    }
    if(!limit) f[p][st] = ret;
    return ret;
}

int main()
{
    n = read();
    while(n)//把n二进制拆分
    {
        a[++len] = n&1;
        n >>= 1;
    }
    memset(f, -1, sizeof(f));
    printf("%lld\n", dfs(1, 0, 1));//还是从高位到低位枚举

    return 0;
}
花神的数论题

C. 手机号码

一下子这么多条件,忽然很蒙圈,我头一次见这么多维度的数组,这告诉我们要有足够的脑洞,我需要什么就开什么。处理三个连续的数的方法是记录上一个和上上个数是什么,一开始让我想的好复杂……还以为要像“启示录”一样记录连续两个和只有一个的情况,也有可能可以不过我没有尝试……果然我颓了。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 33;
const ll mod = 1e7 + 7;

ll l, r, f[11][11][11][2][2][2][2];
int num[12], len;

//p:当前位数,a:上一个数,b:上上个数,c:满足三个连续,d:满足比上限小
ll dfs(int p, int a, int b, bool c, bool d, bool _4, bool _8)
{
    if(_4 && _8) return 0;
    if(p <= 0) return c;
    if(f[p][a][b][c][d][_4][_8] != -1)
    {
        return f[p][a][b][c][d][_4][_8];
    }
    ll res = 0; int lim = !d ? num[p] : 9;
    for(int i=0; i<=lim; i++)
    {
        res += dfs(p-1, i, a, c||(i==b&&i==a), d||(i<lim), _4||(i==4), _8||(i==8));
    }
    return f[p][a][b][c][d][_4][_8] = res;//这一可以直接取等的原因是限制条件都在数组里了??
}

ll calc(ll x)
{
    if(x < 1e10) return 0;
    memset(f, -1, sizeof(f));
    len = 0;//记得清空啊
    while(x)
    {
        num[++len] = x % 10;
        x /= 10;
    }
    /*for(len=0; x; x/=10)
    {
        num[++len] = x % 10;
    }*/
    /*for(int i=1; i<=len; i++)
    {
        printf("%d ", num[i]);
    }
    printf("\n");*/
    //=dfs(11, ...)考虑前导0的另一种方法,跳出循环;上一种方法是传个参数详见T1
    ll res = 0;
    for(int i=1; i<=num[len]; i++)
    {
        res += dfs(10, i, 0, 0, i<num[len], i==4, i==8);
    }
    return res;
}

int main()
{
    scanf("%lld%lld", &l, &r);
    printf("%lld\n", calc(r)-calc(l-1));

    return 0;
}
View Code

D. haha数

这题的难度在于一个数的合法性和整体有关,不能通过记录前某位是什么来直接判断,好像也不能传递,似乎就比上一个连续三位的限制条件更高级……提示说建议我们独立完成……求我心情的矛盾程度?它的位数很不确定好像把所有位都记录下来不太现实?如果改变套路从后往前填?那<=n的条件就不好保证……而且能由末位数字确定能不能整除的好像只有2和5,4要求两位,还有的好像只有除一下才知道……怎么办怎么办我想看题解但我才刚思考了没过15分钟……现在有15分钟了……25分钟了……

提示

数位 dp 入门题,大家最好独立完成。

算了,水平不够,我先跳了……

P3898 [湖南集训]大新闻

用到了数位dp中位数限制的思想,但并不是一个完全的数位dp的题。

#include <bits/stdc++.h>

using namespace std;

typedef long long ll;
const int maxn = 5e4 + 2;
const int mod = 1e9 + 7;
const ll INF = 1e18;

ll n, f;
double p, s, p1, p2;

inline ll read()
{
    ll x = 0, f = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9')
    {
        if(ch == '-')
        {
            f = -1;
        }
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9')
    {
        x = (x << 1) + (x << 3) + (ch^48);
        ch = getchar();
    }
    return x * f;
}

void print(double s)
{
    while(s >= 10)
    {
        f++;
        s/=10;
        //printf("s = %.5lf\n", s);
    }
    printf("%.5lf %lld\n", s, f);
}

double solve1(ll n)
{
    double ret = 0;
    ll Pow = 0, tmp = n-1;
    while(tmp != 0)
    {
        Pow++; tmp >>= 1;
    }
    for(int i=Pow; i>0; i--)
    {
        ll nw = (n>>i)*(1ll<<i-1)+min(n-((n>>i)<<i), 1ll<<i-1);
        double p = double(n-nw)/n;
        ret += (1.0-p)*(1ll<<(i-1))*p;
    }
    return ret * 2.0;
}

double solve2(ll n)
{
    if(n == 1) return 0.0;
    double ret = 0.0;
    ll v = 1ll, delta, num, tmp = n-1;
    n--;
    while(v <= tmp)
    {
        v <<= 1;
    }
    //跳出循环后的v一定比tmp大了一位,-1回到本位
    //它找的是异或结果的上限
    delta = v - 1ll;
    v >>= 1;//<=tmp
    /*
    delta是能做的最大贡献,每一个x都对应了能让它生成最大贡献的y,不过这个y可能超过了
    取值范围,n-1是一个数,它的最高位一定是1,就相当于讨论到“这一位上”,n-1是1的情况
    只要x的最高位是0,后面的就可以随便选,y随着x被唯一确定也就等于x的情况数
    x最高位是1一共有v种选法,选法总数是n+1,本来是n的,后来-1了就加回来
    如果x的最高位是1,我还想让当前位做贡献,保险起见并且因为问题可以往后丢,
    所以就算只有当前位贡献的,x=0并且y=1,前面的已经卡死,如果从这一位开始后面每个
    y都和n-1对应相等,那就一定不会超范围,y只有一种情况,x有v种,贡献是v

    所以这两个ret都是加的贡献值*方案数
    */
    ret += (double)delta*(n-v+1);
    ret += (double)v*v;
    num = v; delta >>= 1;
    /*
    考虑完最高位,接着下面的,从一个问题中分出了子问题一定n-1的第i位为1,
    x的第i位为0导致有了后面的限制,否则所有的情况都已经算进来了,刚才需要拆分的问题只计算了
    本位,不过y不需要和n-1的每一位都对应相等,只要高位有一位更小就能解锁
    */
    //我们考虑最高的i-1位和n-1的前i-1位相同时所有的x对答案的贡献
    while(v != 1)
    {
        v >>= 1; delta >>= 1;
        if(n & v)//当前位上n-1=1
        {
            ret += (double)num * v;
            //什么都没有固定,我只要单独的求可以使这一位有贡献的情况,x随便选
            //任意的x一定对应一个合法的每一位与n-1对应相等的y使得当前位有贡献
            ret += (double)(num>>1)*delta;
            //固定了当前位x只能选1,就少了一步有两种情况的分类讨论,/2
            //由于上一个没有分类讨论,当前位有贡献的已经算过就不用再算了
            //这大概是delta连续右移两次的原因
            //为什么x=1和x=0的情况可以交叉合并?因为无论是哪一种,x对应的y都是唯一确定
            //所以最终的情况数算的都是x可能的取值
            num >>= 1;
            //问题规模减小
        }
        else 
        {
            //我还是想让当前位做贡献,后面y和n-1对应相等都不够,
            //必须固定之前的某一位x和y同时是0,因为x本来就是和y一一对应的
            //所以只删掉x在那一位上是1的情况就好
            ret += (double)(num>>1)*v;
        }
    }
    return ret / (double)(n+1);
}

int main()
{
    n = read();
    scanf("%lf", &p);
    p1 = solve1(n); p2 = solve2(n);
    s = (1.0-p)*p1+p*p2;
    printf("%.6lf\n", s);
    //print(s);

    return 0;
}
View Code

 

posted @ 2022-08-08 17:16  Catherine_leah  阅读(58)  评论(3编辑  收藏  举报
/* */