小學生都能看懂的數位dp


簡介

數位\(dp\)歸為計數\(dp\),通常需要統計一個區間\([L,R]\)內滿足某些限制條件的個數

前言

數位dp其實很久前就知道了,也做過幾道和其他算法混在一起的題目,其實通過手玩是能做的

但畢竟是種算法,還是系統學下比較好(節省手玩時間)

模板題

P2602 [ZJOI2010]數字計數

\([L,R]\)間,\(0\)\(9\)在數位上出現的次數

設數組\(dp_i\)為滿\(i\)位每個數字出現的次數,也是說四位我們都算進去而忽略前導0的存在
而實際中顯然像0012這樣的數字我們是不會統計那兩個0的
(滿位情況下每個數字出現次數相同故我們可以公用空間)

\(dp_i=dp_{i-1}+10^{i-1}\),理解??
比如從一位到兩位:
\(1.\)\(0\)~\(9\)在第一位各出現一次,到兩位時它們前面補上\(0\)~\(9\)(第一位為0:00,10......90)故在\(dp_{i-1}\)基礎上乘\(10\)
\(2.\)不僅要算原有位出現的次數,新加的該位也要算(第二位為1:10,11......19),數滿\(i-1\)位,故加上\(10^{i-1}\)

當然你也可以手玩一遍驗證一下

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL dp[20],tmp[20],a[20];
int main(){
    dp[0]=0,tmp[0]=1;
    for(LL i=1;i<=4;++i){
    	dp[i]=dp[i-1]*10+tmp[i-1],
		tmp[i]=tmp[i-1]*10;
	}
	for(LL i=0;i<=9999;++i)
		for(LL j=1,bit=i;j<=4;++j)
			++a[bit%10],
			bit/=10;
	printf("%lld",dp[4]);printf("\n");
	for(LL i=0;i<=9;++i)
	    printf("%lld ",a[i]);printf("\n");
    return 0;
}

現在來考慮前導\(0\),第\(i\)位為前導\(0\)時,實際上我們多算了填滿\(i-1\)位的次數,減去就好
My complete code(代碼有少量注釋,應該容易看懂)

#include<cstdio>
#include<iostream>
#include<cstring>
#include<string>
#include<algorithm>
using namespace std;
typedef long long LL;
inline LL Read(){
	LL x(0),f(1);char c=getchar();
	while(c<'0'||c>'9'){if(c=='-')f=-1;c=getchar();}
	while(c>='0'&&c<='9')x=(x<<3)+(x<<1)+c-'0',c=getchar();
	return x*f;
}
LL l,r;
LL a[20],cover[20],dp[20],countl[20],countr[20];
inline void Solve(LL num,LL *A){
	LL len(0),bit=num;
	while(num){
		a[++len]=num%10,num/=10;
	}
	for(LL i=len;i>=1;--i){
		for(LL j=0;j<10;++j)
		    A[j]+=dp[i-1]*a[i];//有第i位時的貢獻 
		for(LL j=0;j<a[i];++j)
		    A[j]+=cover[i-1];//i-1位以下的貢獻 
		bit-=a[i]*cover[i-1],
		A[a[i]]+=bit+1,//第i位上的該數未補滿i-1位 
		A[0]-=cover[i-1];//減去前導0的個數 
	}
}
int main(){
	cover[0]=1;
	for(LL i=1;i<=16;++i){
		dp[i]=(dp[i-1]<<3)+(dp[i-1]<<1)+cover[i-1],
		cover[i]=(cover[i-1]<<3)+(cover[i-1]<<1);
	}
	l=Read(),r=Read(),
	Solve(l-1,countl),
	Solve(r,countr);
	for(LL i=0;i<10;++i)
	    printf("%lld ",countr[i]-countl[i]);
	return 0;
} 

再用類似的方法做道題

[SCOI2009]windy數

\([L,R]\)間不含前導零且相鄰兩個數字之差至少為2的個數
我們用\(dp_{i,j}\)表示前\(i\)位,第\(i\)位為\(j\)的方案數\((\)稍微做過點動規的人這個都不成問題吧\()\)

考慮\(Solve(x)\)為求\(x\)以內的答案
分三部分做\((\)\(len\)為數位,每一位為\(a_i)\)

  • 只有\(len-1\)位的答案\(\sum\limits_{i=1}^{len-1}\sum\limits_{j=1}^9 dp_{i,j}\)

  • \(len\)位,但最高位未頂着上界,答案為\(\sum\limits_{i=1}^{a_{len}-1} dp_{len,i}\)

  • \(len\)位,且從最高位到\(i+1\)連續一段都頂着上界,最后答案為\(\sum\limits_{i=1}^{len-1}\sum\limits_{j=0}^{a_i-1}dp_{i,j}\)
    但我們發現頂着上界的那部分也得滿足條件\((\)差值至少為\(2)\)

但我們發現其實是在計算\(x-1\)以內的答案\((\)第三部分使得最低位統計不到上界\()\)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL l,r;
LL dp[15][10],a[15];
inline void Calc(){
	for(LL i=0;i<=9;++i) dp[1][i]=1;
	for(LL i=2;i<=10;++i)
	    for(LL j=0;j<=9;++j)
	        for(LL k=0;k<=9;++k)
	            if(abs(j-k)>=2) 
				    dp[i][j]+=dp[i-1][k];
}
inline LL Solve(LL x){
	LL len(0),tmp(x),ret(0);
	while(tmp) a[++len]=tmp%10,tmp/=10;
	for(LL i=len-1;i>=1;--i){
	    for(LL j=0;j<a[i];++j) 
		    if(abs(j-a[i+1])>=2)
			    ret+=dp[i][j];
		if(abs(a[i]-a[i+1])<2) break;
	} 
	for(LL i=1;i<a[len];++i) ret+=dp[len][i];//最高位為1~x-1 
	
	for(LL i=1;i<len;++i) 
	    for(LL j=1;j<=9;++j)
		    ret+=dp[i][j];
	return ret;
}
int main(){
	Calc();
	scanf("%lld%lld",&l,&r);
	printf("%lld",Solve(r+1)-Solve(l));
	return 0;
}

更無腦的方法

用記憶化搜索來做,拋開循環后轉移狀態能更加隨意,大部分數位動規的題都可隨意切

拿上題舉例,我們用\(Dfs(now,num,top)\)表示遍歷到第\(now\)位,上一位為\(num\),是否頂着上界
每層確定這一位選啥,判斷是否和上一位沖突,全部確定完了方案數\(+1\)

  • 由於頂着上界是比較特殊的情況,所以這類答案單獨計算,不用記憶化,其他(不頂着上界)的情況用\(f_{now,num}\)表示\(Dfs(now,num,0)\)的答案
  • 最后算一下最高位為\(0\)的情況
#include<bits/stdc++.h>
typedef long long LL;
LL l,r;
LL f[11][10],a[11],dp[11][10];
inline void Calc(){
    for(LL i=0;i<=9;++i) dp[1][i]=1;
    for(LL i=2;i<=10;++i)
        for(LL j=0;j<=9;++j)
            for(LL k=0;k<=9;++k)
                if(abs(j-k)>=2) 
                    dp[i][j]+=dp[i-1][k];
}
LL Dfs(LL now,LL num,LL top){
    if(!now) return 1;
    if(!top && f[now][num]) return f[now][num];
    LL Up(top?a[now]:9),ret(0);
    for(LL i=0;i<=Up;++i) 
        if(abs(num-i)>=2)
            ret+=Dfs(now-1,i,top&&(i==Up));
    if(!top) f[now][num]=ret;
    return ret;
}
inline LL Solve(LL num){
    LL tot(0);
    while(num){
        a[++tot]=num%10;
        num/=10;
    }
    LL ret(0);
    for(LL i=1;i<=a[tot];++i) ret+=Dfs(tot-1,i,i==a[tot]);
    for(LL i=tot-1;i>=1;--i)
        for(LL j=1;j<=9;++j)
            ret+=dp[i][j];
    return ret;
}
int main(){
    Calc();
    scanf("%lld%lld",&l,&r);
    printf("%lld\n",Solve(r)-Solve(l-1));
    return 0;
}

其他題目

搜索的方法相比純循環計算,由於上道題限制較少難度簡單,故顯得有點雞肋

[CQOI2016]手機號碼

11位的手機號,在\([L,R]\)求出滿足兩個條件的方案數:至少有三位連續的數字相同,不能同時出現\(4\)\(8\)

\(Dfs(now,p,pp,\_4,\_8,top,hw)\)
分別表示到第幾位了,前一位數字,前兩位數字,是否出現過\(4/8\),是否頂着上界,是否出現過了三連

用冗長的七重循環直接番了五倍的代碼量

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL l,r;
LL f[12][10][10][2][2][2],a[20];
LL Dfs(LL now,LL p,LL pp,LL _4,LL _8,LL top,LL hw){
	if(_4&&_8) return 0;
	if(!now) return hw;
	if(!top && f[now][p][pp][_4][_8][hw]!=-1) return f[now][p][pp][_4][_8][hw];
	LL Up=top?a[now]:9;
	LL ret(0);
	for(LL i=0;i<=Up;++i)
	    ret+=Dfs(now-1,i,p, _4|(i==4),_8|(i==8), top&&(i==Up) ,hw|(i==pp&&i==p));
	if(!top) f[now][p][pp][_4][_8][hw]=ret;
	return ret;
}
inline LL Solve(LL x){
	LL tot(0);
	while(x){
		a[++tot]=x%10;
		x/=10;
	}
	if(tot!=11) return 0;
	LL ret(0);
	for(LL i=1;i<=a[tot];++i)
	    ret+=Dfs(tot-1,i,0,(i==4),(i==8),i==a[tot],0);
	return ret;
}
int main(){
	cin>>l>>r;
	memset(f,-1,sizeof(f));
	cout<<Solve(r)-Solve(l-1);
	return 0;
}

總結

搜索與循環異曲同工之妙,但前者更易轉移狀態,在限制較多的情況下被大部分人喜愛,但后者更能鍛煉碼力權衡利弊后均可放心食用

搜索的常見模板

LL Dfs(LL now,限制,LL top){
	if(!now) return 判斷條件;
	if(!top && f[now][...]) return f[now][...];
	LL Up=top?a[now]:9;
	LL ret(0);
	for(LL i=0;i<=Up;++i)
	    ret+=Dfs(now-1,...,top&(i==Up));
	if(!top) f[now][...]=ret;
	return ret;
}
inline LL Solve(LL x){
	LL tot(0);
	while(x){
		a[++tot]=x%10;
		x/=10;
	}
	Dfs(....);
	return ret;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM