淺談數位DP
前言
李老師太巨啦!!!
帶着一罐剛喝完的紅牛的李老師走進教室
xaero:“紅牛喝不喝"
李老師:“不喝不喝,再喝要猝死了”
於是李老師帶走了兩罐新的紅牛的空罐子
李老師講課的時候談到了數位dp,然後發現好久沒弄過了……於是去淺談了一下。
數位DP是什麼
一般數位DP是用於計數的DP,一般用於求之間滿足某種規則(設規則爲),也就是滿足的數有多少個
數位的含義:一個數有個位、十位、百位、千位…數的每一位就是數位的意思
首先數位DP有兩種實現形式,遞推與記憶化搜索,當然選取於數據的組數。
數位DP的大概如何操作
因爲我的水平有限,故只能給出“大概”
首先給出對於簡單求上述模型,“一般用於求之間滿足某種規則(設規則爲),也就是滿足的數有多少個”:
for(int i=l;i<=r;i++)
if(right(i)) ans++;
然鵝這樣的暴力是的,
那麼,我們加上記憶化搜索的枚舉:
控制上屆()枚舉,從最高位向下枚舉,用DP方程的形式也就是表示枚舉到第位(從高位往低位)時,當前這一位的數字爲,有多少個符合方案的數
那麼,以數字243爲例枚舉,
1.當最高位是0時,對於這個回一個(判斷前導0)的變量標記,(前導0對於計數的影響要看具體題目的,有比較大影響的比如說數字計數(ZJOI2010))
2.最高位是1時,後面自然可以枚舉~,
3.最高位是2時,爲了防止計了其他的數,第二位就有,也就是上限,第二位只能枚舉到4,如果第二位枚舉到4了,那麼第三位只能枚舉1以此類推。
一般數位DP的主程序如下,
就是到右邊界所有的符合條件的數,減去到左邊界-1符合條件的所有數(至於爲什麼不直接處理中間呢,是因爲比較難以實現,要限制的東西太多)
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,求之間不包含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 數字統計
註釋放代碼裏
#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));
}
總結:
大概是寫完了?
等我模擬賽打完回來完善一下~