淺談數位DP

淺談數位DP

前言

李老師太巨啦!!!
帶着一罐剛喝完的紅牛的李老師走進教室
xaero:“紅牛喝不喝"
李老師:“不喝不喝,再喝要猝死了”
於是李老師帶走了兩罐新的紅牛的空罐子

李老師講課的時候談到了數位dp,然後發現好久沒弄過了……於是去淺談了一下。

數位DP是什麼

一般數位DP是用於計數的DP,一般用於求[l,r][l,r]之間滿足某種規則(設規則爲g(x)g(x)),也就是滿足g(x)g(x)的數有多少個
數位的含義:一個數有個位、十位、百位、千位…數的每一位就是數位的意思
首先數位DP有兩種實現形式,遞推與記憶化搜索,當然選取於數據的組數。

數位DP的大概如何操作

因爲我的水平有限,故只能給出“大概”
首先給出對於簡單求上述模型,“一般用於求[l,r][l,r]之間滿足某種規則(設規則爲g(x)g(x)),也就是滿足g(x)g(x)的數有多少個”:

for(int i=l;i<=r;i++)  
        if(right(i)) ans++;  

然鵝這樣的暴力是O(nright)O(n*right)的,

那麼,我們加上記憶化搜索的枚舉:
控制上屆(limitlimit)枚舉,從最高位向下枚舉,用DP方程的形式也就是f[i][j]f[i][j]表示枚舉到第ii位(從高位往低位)時,當前這一位的數字爲jj,有多少個符合方案的數
那麼,以數字243爲例枚舉,
1.當最高位是0時,對於這個回一個frontzerofrontzero(判斷前導0)的變量標記,(前導0對於計數的影響要看具體題目的,有比較大影響的比如說數字計數(ZJOI2010)
2.最高位是1時,後面自然可以枚舉00~99
3.最高位是2時,爲了防止計了其他的數,第二位就有limitlimit,也就是上限,第二位只能枚舉到4,如果第二位枚舉到4了,那麼第三位只能枚舉1以此類推。

一般數位DP的主程序如下,
就是到右邊界所有的符合條件的數,減去到左邊界-1符合條件的所有數(至於爲什麼不直接處理中間呢,是因爲比較難以實現,要限制的東西太多lerler

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

接下來給出數位DP的模板,講解基本全在模板裏了

#include<bits/stdc++.h> 
using namespace std;
int f[12][10],n,m,a[12];
int dp(int len,int pre,bool limit,bool frontzero)//len是枚舉的位,limit是上限,
//frontzero是
//前導0的判斷,pre是前一位是什麼
//數,也就是用來判斷某些條件的,比如說不要六二,
//pre爲6的時候,這一位就不能是2
{
	if (len==0) return 1;//枚舉到個位的後一位那麼直接可以退出了
	if (!frontzero&&!limit&&f[len][pre]!=-1) return f[len][pre];//如果
	//前導都不是0,
	//且也不受上限限制,那麼f[len][pre]也有
	//值,那麼就這個狀態可以直接作爲f[len+1][pre](即位數爲len+1,
	//這一位的數字爲pre)返回
	int p,ans=0,maxx=(limit?a[len]:9);//有上限時,只能是這一位原數字上
	//的數字,否則爲9
	for (int i=0; i<=maxx; i++)
	  {
	  	if () continue;//視具體題目條件而定
	  	p=i;
	  	if (frontzero&&i==0) p=-10000;//如果一直是0,那麼把p設置
	  	//成一個絕對爲f[len-1][p]
	  	//絕對爲-1的狀態,讓後面不能過繼。
	  	ans+=dp(len-1,p,limit&&(i==maxx),(p==-10000));//把有沒有受
	  	//上限控制,前導是不是都爲0,
	  	//前導都爲0的狀態的話一定要單獨
	  	//做
	  }
	if (!frontzero&&!limit) f[len][pre]=ans;//記憶化搜索
	return ans;
}
 

inline int solve(int x)
{
	int numx=0;
	memset(a,0,sizeof(a));
	while (x)
	  {
	  	a[++numx]=x%10;
	  	x/=10;
	  }//把x的位數及每一位上是什麼記錄下來
	memset(f,-1,sizeof(f));
	return dp(numx,-10000,1,1);
}
int main()
{
	scanf("%d%d",&n,&m);
	printf("%d",solve(m)-solve(n-1));
}

例題

T1

著名經典數位DP,不要62,求[l,r][l,r]之間不包含62和4的數字個數

#include<iostream>
#include<cstdio>
#include<cstring>
#include<string>
using namespace std;
typedef long long ll;
int a[20];
int dp[20][2];
int dfs(int pos,int pre,int sta,bool limit)
{
    if(pos==-1) return 1;
    if(!limit && dp[pos][sta]!=-1) return dp[pos][sta];
    int up=limit ? a[pos] : 9;
    int tmp=0;
    for(int i=0;i<=up;i++)
    {
        if(pre==6 && i==2)continue;
        if(i==4) continue;
        tmp+=dfs(pos-1,i,i==6,limit && i==a[pos]);
    }
    if(!limit) dp[pos][sta]=tmp;
    return tmp;
}
int solve(int x)
{
    int pos=0;
    while(x)
    {
        a[pos++]=x%10;
        x/=10;
    }
    return dfs(pos-1,-1,0,true);
}
int main()
{
    int le,ri;
    while(~scanf("%d%d",&le,&ri) && le+ri)
    {
        memset(dp,-1,sizeof dp);
        printf("%d\n",solve(ri)-solve(le-1));
    }
    return 0;
}

T2

windy數
大概是相鄰兩位差不能小於等於2的數

#include<bits/stdc++.h> 
using namespace std;

int f[12][10],n,m,a[12];

int dp(int len,int pre,bool limit,bool frontzero)
{
	if (len==0) return 1;
	if (!frontzero&&!limit&&f[len][pre]!=-1) return f[len][pre];
	int p,ans=0,maxx=(limit?a[len]:9);
	for (int i=0; i<=maxx; i++)
	  {
	  	if (abs(i-pre)<2) continue;
	  	p=i;
	  	if (frontzero&&i==0) p=-10000;
	  	ans+=dp(len-1,p,limit&&(i==maxx),(p==-10000));
	  }
	if (!frontzero&&!limit) f[len][pre]=ans;
	return ans;
}
 

inline int solve(int x)
{
	int numx=0;
	memset(a,0,sizeof(a));
	while (x)
	  {
	  	a[++numx]=x%10;
	  	x/=10;
	  }
	memset(f,-1,sizeof(f));
	return dp(numx,-10000,1,1);
}
int main()
{
	scanf("%d%d",&n,&m);
	printf("%d",solve(m)-solve(n-1));
}

T3

ZJOI 數字統計
註釋放代碼裏lerler

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
ll f[15][2][15][2];
int num[N];  //num來存這個數每個位子上的數碼
/*
記憶化搜索。
len是當前爲從高到低第幾位。
limited表示當前位是否和num[len]相等,
0是相等,1是不相等。
sum表示當前數字出現的次數。
frontzero表示之前是否是前導0。
d是當前在算的數碼。
*/
ll dfs(int len, bool limited, int sum, bool frontzero, int d)
{
    ll ans=0;
    if (len==0) return sum;  //邊界條件
    if (f[len][limited][sum][frontzero] != -1) return f[len][limited][sum][frontzero];  //記憶化
    for (int i = 0; i < 10; i ++)
	  {
        if (!limited && i > num[len]) break;
        /*
        由於我們是從高位到低位枚舉的,
        所以如果之前一位的數碼和最大數的數碼相同,
        這一位就只能枚舉到num[len];
        否則如果之前一位比最大數的數碼小,
        那這一位就可以從0~9枚舉了。
        */
        ans+=dfs(len-1,limited||(i<num[len]),sum+((!frontzero||i)&&(i==d)),frontzero&&(i==0),d);
        /*
        繼續搜索,數位減一,
        limited的更新要看之前有沒有相等,
        且這一位有沒有相等;
        sum的更新要看之前是否爲前導0或者這一位不是0;
        frontzero的更新就看之前是否爲前導0且這一位繼續爲0;
        d繼續傳進去。
        */
      }
    f[len][limited][sum][frontzero] = ans;
    //記憶化,
    //把搜到的都記下來
    return ans;
}

ll solve(ll x, int d)
{
    int len = 0;
    while (x)
	  {
        num[++len]=x%10;
        x/=10;
      } 
    memset(f,-1,sizeof f); 
    return dfs(len, 0, 0, 1, d); 
	//開始在第len位上,
	//最高位只能枚舉到num[len]所以limited是0,
	//sum=0,有前導0。
}

int main()
{
    ll a,b; 
    scanf("%lld%lld", &a, &b);
    for (int i=0;i<10;i++)
        printf("%lld ",solve(b,i)-solve(a-1,i));
}

總結:
大概是寫完了?
等我模擬賽打完回來完善一下~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章