數位DP複習筆記
前言
複習筆記第五篇。(由於某些原因(見下),放到了第六篇後面更新)CSP-S RP++.
luogu 的難度評級完全不對,所以換了順序,換了別的題目。有點亂,見諒。要罵就罵洛谷吧,原因在T2處
注:此文大部分建議結合程式碼閱讀,便於解釋
0——HDU 3555 Bomb
題意
求 \(1\sim N\) 中包含子串 49
的個數。\(N\leq 2^{63}-1.\)
思路
設 \(f[i][0]\) 表示長度為 \(i\) ,不含有 49
的個數;\(f[i][1]\) 為最高位為 9
但不含 49
的個數;\(f[i][2]\) 表示含有 49
的個數。轉移方程為:
對於每一個位數,預處理出 \(f\) 陣列,然後按位分割 \(n\) 並進行統計。具體見程式碼註釋。
程式碼
#include <bits/stdc++.h> #define ll long long using namespace std; ll f[30][3]; int a[30]; void init() { f[0][0]=1; f[0][1]=f[0][2]=0; for ( int i=1; i<25; i++ ) { f[i][0]=10*f[i-1][0]-f[i-1][1]; f[i][1]=f[i-1][0]; f[i][2]=10*f[i-1][2]+f[i-1][1]; } } ll calc( ll x ) { int len=0; while ( x ) a[++len]=x%10,x/=10; a[len+1]=0; ll res=0; bool fl=0; for ( int i=len; i>=1; i-- ) { res+=f[i-1][2]*a[i]; //先加上低位上面有49的個數。 if ( fl ) res+=f[i-1][0]*a[i]; //之前如果出現過49,那麼加上長度為 i-1 的不符合的個數 else if ( a[i]>4 ) res+=f[i-1][1]; //之前沒有出現過49,但是這一位可以填4,那麼累加上之前有9的情形 if ( a[i+1]==4 && a[i]==9 ) fl=1; //判斷是否出現過49 } if ( fl ) res++; //加上本身 return res; } int main() { int T; scanf( "%d",&T ); init(); while ( T-- ) { ll x; scanf( "%lld",&x ); printf( "%lld\n",calc( x ) ); } return 0; }
1——P2602 [ZJOI2010]數字計數
題意
給定兩個正整數 a 和 b,求在 \([a,b]\) 中的所有整數中,每個數碼(digit)各出現了多少次。\(a,b\leq 1e12\)
思路
問題轉化為在 \([1,n]\) 中求出數碼出現次數,字首和的思想,相減即可。其實這樣的題更多的不是統計而是填數的思想。
具體如何處理統計見程式碼註釋,結合程式碼理解。
注:資料範圍很小,本來是放在後面的,但是發現T1反倒是加強版所以就挪到前面來了,下一題是加強。程式碼是下一題賀過來的,所以可能有些多此一舉的操作。不過註釋都放在這題上了。
這道題就是用來告訴我們 ZJOI的某些題也是可以爆切的(
程式碼
#include <bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int N=2e5+10;
ll ans,a[N],cnt[N],r1[N],r2[N],x[N];
void count( ll num,ll *s )
{
int len=0; ll sav=num;
while ( num ) a[++len]=num%10,num/=10;
num=sav;
for ( int i=len; i>=1; i-- )
{
for ( int j=0; j<=9; j++ ) //1~i-1位的貢獻乘上當前位的方案數
s[j]+=a[i]*cnt[i-1];
for ( int j=0; j<a[i]; j++ ) //作為第 i 位的 0~a[i]-1的貢獻
s[j]+=x[i-1];
num-=a[i]*x[i-1];
s[a[i]]+=num+1; s[0]-=x[i-1]; //a[i]加上作為當前位的貢獻,處理前導0
}
}
int main()
{
x[0]=1;
for ( int i=1; i<=19; i++ ) //預處理10的冪次和
{
cnt[i]=(cnt[i-1]*10)+x[i-1]; //1~10^i-1出現次數
x[i]=x[i-1]*10;
}
memset( a,0,sizeof(a) );
memset( r1,0,sizeof(r1) ); memset( r2,0,sizeof(r2) );
ll x,y; scanf( "%llu%llu",&x,&y );
count( x-1,r1 ); count( y,r2 ); ans=0;
for ( int i=0; i<=9; i++ )
printf( "%lld ",r2[i]-r1[i] );
return 0;
}
2——P4999 煩人的數學作業
感謝18年的老王供題,成為luogu最水的數位DP,唯一一道綠題
題意
給定一個區間 \([L,R]\) ,求其中每個數的數字和。 \(1\leq L\leq R\leq 1e18\) ,答案 \(\mod 1e9+7\)
思路
ZJOI的加強版,紫題變成綠題……無語。這是難度評級又不是來源評級啊。
求數字和,那麼就是求 \(count(i\in [L,R])\times i,1\leq i\leq 9\) ,字首和即可。複雜度 \(O(\log n)\)
貌似要開 unsigned long long
,我也不知道我哪裡溢位了。注意 ull
的輸出格式是 %llu
.
程式碼
#include <bits/stdc++.h>
#define ll unsigned long long
using namespace std;
const int N=2e5+10,mod=1e9+7;
ll ans,a[N],cnt[N],r1[N],r2[N],x[N];
void count( ll num,ll *s )
{
int len=0; ll sav=num;
while ( num ) a[++len]=num%10,num/=10;
num=sav;
for ( int i=len; i>=1; i-- )
{
for ( int j=0; j<=9; j++ )
s[j]+=a[i]*cnt[i-1];
for ( int j=0; j<a[i]; j++ )
s[j]+=x[i-1];
num-=a[i]*x[i-1];
s[a[i]]+=num+1; s[0]-=x[i-1];
}
}
int main()
{
int T; scanf( "%d",&T );
x[0]=1;
for ( int i=1; i<=19; i++ )
{
cnt[i]=(cnt[i-1]*10)+x[i-1];
x[i]=x[i-1]*10;
}
while ( T-- )
{
memset( a,0,sizeof(a) );
memset( r1,0,sizeof(r1) ); memset( r2,0,sizeof(r2) );
ll x,y; scanf( "%llu%llu",&x,&y );
count( x-1,r1 ); count( y,r2 ); ans=0;
for ( int i=1; i<=9; i++ )
(ans+=1ll*i*(r2[i]-r1[i]+mod)%mod)%=mod;
printf( "%llu\n",ans%mod );
}
return 0;
}
3——P4317 花神的數論題
題意
設 \(\text{sum}(i)\) 表示 \(i\) 的二進位制表示中 \(1\) 的個數。給出一個正整數 \(N\) ,求 \(\prod_{i=1}^{N}\text{sum}(i)\) 。
思路
換一種角度看這個乘積,會發現就相當於統計出 \(1\sim N\) 中 1 的個數為 \(k\) 的數量 \(cnt_k\) ,然後 \(\prod k^{cnt_k}\) 即可。
(怎麼那麼水啊,這都什麼垃圾紫題,題白挑了)為了讓這道題更有價值,程式碼實現非常的神仙。Orz粉兔。
粉兔的程式碼看了很久才理解……luogu上至今沒有看到公開的詳解。
這裡註釋的是我認為正確的理解,若有差錯還請指正。
程式碼
#include <cstdio>
#define ll long long
const ll mod=1e7+7;
ll n,ans=1,cnt,f[50];
ll power( ll a,ll b )
{
ll res=1;
for ( ; b; b>>=1,a=a*a%mod )
if ( b&1 ) res=res*a%mod;
return res;
}
int main()
{
scanf( "%lld",&n );
cnt=0; f[0]=0;
for ( int len=49; ~len; --len )
{
for ( int i=49; i; --i )
f[i]+=f[i-1];
if ( n>>len&1 ) f[cnt]++,cnt++;
//cnt記錄的是除了現在這一位,之前有的1的個數,f[cnt]++表示,這一位的1產生了一種使得前面的1全部能取到的方案。
}
f[cnt]++; //加上本身
//之前一直想不明白,如果這樣列舉,為什麼能直接從49開始。
//一開始的想法是預支最高位的1,這樣當前每次加一位就能取1,對應 f[i-1] 到 f[i] 的轉移
//但是這樣有個問題,就是最高位沒有1了怎麼辦,這樣預支無效,答案就會偏大
//後來發現,關鍵在外層迴圈。當位數大於二進位制下n的位數的時候,f始終為0,最後一句if 不會執行,也就不會出現上述問題。
//一旦開始累加出現了值,那麼一定就是有高位可以預支了。否則 if 中的等號不會成立。
for ( int i=1; i<=49; ++i )
ans=ans*power( i,f[i] )%mod;
printf( "%lld",ans );
return 0;
}
4——P6218 [USACO06NOV] Round Numbers S
題目連結 luogu
題意
問區間 \([l,r]\) 中有多少個數的二進位制表示中,0 的數目不小於 1 的數目。
思路
數位DP。
可以發現,這一題和上一題都有“區間詢問滿足某種條件的數的數量”的形式,而且和數的數位有關,可以把這個總結為數位DP的一種常見型別。
設 \(f[i][j][k]\) 表示 \(i\) 位二進位制,有 \(j\) 個1,最後一位為 \(k\) 的數量。
\[f[i][j][0]=f[i-1][j][0]+f[i-1][j][1](j<i) \\\\ f[i][j][1]=f[i-1][j-1][0]+f[i-1][j-1][1](j>0) \\\\ f[1][0][0]=1,f[1][1][1]=1 \]設最終答案 \(g(x)\) 表示區間 \([1,x)\) 的答案個數,那麼所求的給定區間答案就是 \(g(R+1)-g(L)\) .把 \(x\) 轉成二進位制,並設長度為 \(len\).
首先將長度小於 \(len\) 的累加(因為位數小於的都可以按位數直接統計)。然後考慮位數相等的(這時候,你統計出來的數顯然最高位都是1.)對除了首位的所有位,判斷是否為1.如果是,那麼存在一些數,長度為 \(len\) ,值小於 \(x\) ,且二進位制表示中這一位開始比 \(x\) 小。統計即可。(非常經典的套路,類似“餘數”一樣的統計。)
程式碼
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N=40;
ll f[N][N][2];
int a[N],b[N],lena,lenb;
ll solve( int *s,int len )
{
ll res=0; int cnt0=0,cnt1=1;
for ( int i=len-1; i>=1; i-- )
for ( int j=0; j<=(i>>1); j++ )
res+=f[i][j][1];
for ( int i=len-1; i>=1; i-- )
{
if ( s[i] ) for ( int j=0; j<=i; j++ )
if ( cnt0+i-j>=cnt1+j ) res+=f[i][j][0];
if ( s[i] ) cnt1++;
else cnt0++;
}
return res;
}
int main()
{
int t1,t2; scanf( "%d%d",&t1,&t2 ); t2++;
for ( ; t1; t1>>=1 ) a[++lena]=t1&1;
for ( ; t2; t2>>=1 ) b[++lenb]=t2&1;
while ( !a[lena] ) lena--;
while ( !b[lenb] ) lenb--;
f[1][0][0]=f[1][1][1]=1;
for ( int i=2; i<=lenb; i++ )
for ( int j=0; j<=i; j++ )
{
if ( j<i ) f[i][j][0]=f[i-1][j][1]+f[i-1][j][0];
if ( j ) f[i][j][1]=f[i-1][j-1][0]+f[i-1][j-1][1];
}
printf( "%lld",solve(b,lenb)-solve(a,lena) );
return 0;
}
5——HDU3652 B-number
題意
求 \(1\sim n\) 中含有 13
且被13整除的數的個數。\(n\leq 1e9\)
思路
含有 13
之前已經有過了,很好處理;考慮如何處理被13整除。那麼可以在原有的 dp 上加一維,設 \(f[i][j][k]\) 表示 \(i\) 位數,\(k=0\) 表示不含 13
,\(k=1\) 表示最高位為 3
,\(k=2\) 表示含有 13
.
如果直接設 \(j\) 表示是否被13整除的話,沒法轉移。考慮設 \(j\) 為當前數 \(\mod 13\) 的餘數,問題就迎刃而解了。
程式碼
#include <bits/stdc++.h>
#define ll long long
using namespace std;
ll f[15][13][3],a[15],n,len;
void init()
{
memset( f,-1,sizeof(f) );
ll sav=n; len=0;
for ( ; sav; sav/=10 ) a[++len]=sav%10;
a[len+1]=0;
}
ll dfs( ll pre,ll pos,ll mo,ll fl,bool lim )
{
if ( pos==0 ) return (fl==2 && mo==0);
if ( !lim && f[pos][mo][fl]!=-1 ) return f[pos][mo][fl];
ll ceil=lim ? a[pos] : 9,res=0;
for ( int i=0; i<=ceil; i++ )
{
int n1,nmo=(mo*10+i)%13;
if ( fl==2 || pre==1 && i==3 ) n1=2;
else if ( i==1 ) n1=1;
else n1=0;
res+=dfs( i,pos-1,nmo,n1,lim&&i==ceil );
}
if ( !lim ) f[pos][mo][fl]=res;
return res;
}
int main()
{
while ( cin>>n )
{
init(); printf( "%lld\n",dfs( -1,len,0,0,1 ) );
}
}
6——HDU6148 Valley Numer
題意
當一個數字,從左到右依次看過去數字沒有出現先遞增接著遞減的“山峰”現象,就被稱作 Valley Number。求 \(1\sim n\) 中的 VN個數。
\(len(N)\leq 100,\mod 1e9+7\)
思路
很典型的一道數位DP。其實這種問題基本就是,只要細節不出問題,考慮全面就好了 哪像某些毒瘤
對於一個狀態,記錄三個資訊:
- 當前位置
pos
- 前導數字
pre
- 增減性
state
,0表示不確定增或者減,1表示增,2表示減
在 dfs 裡面記錄額外兩個資訊:是否處於“前導0序列”中,和之前的每一位是否都去了上限(一旦有一位沒取,那麼後面就可以任意填寫而不需要擔心邊界問題,所以是 &&
)
具體如何計數見程式碼註釋。
程式碼
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int mod=1e9+7,N=110;
int n,a[N],pos;
char s[N];
ll f[N][10][3];
//pos,pre,0無增減,1增,2減
ll dfs( int pos,int pre,int state,bool lead,bool lim ) //位置,前導,狀態,前導0,上限
{
if ( pos==-1 ) return lead ? 0 : 1;
if ( !lead && !lim && f[pos][pre][state] ) return f[pos][pre][state];
int up=lim ? a[pos] : 9; ll res=0;
for ( int i=0; i<=up; i++ )
{
if ( lead )
{
if ( i==0 ) res=(res+dfs( pos-1,0,0,1,0 ))%mod;
//還是前導0,繼續
else res=(res+dfs( pos-1,i,0,0,(i==a[pos] && lim) ))%mod;
//這一位不是0了,那麼要判斷這一位的上限和之前有沒有上限
}
else
{
if ( i<pre ) //減
{
if ( state==1 ) continue;
res=(res+dfs( pos-1,i,2,0,lim && i==a[pos]) )%mod;
}
else if ( i==pre ) res=(res+dfs( pos-1,i,state,0,lim && i==a[pos] ) )%mod;
//不增不減,注意這裡要繼承之前的增減性而不能簡單為0
else res=(res+dfs( pos-1,i,1,0,lim && i==a[pos] ))%mod; //增
}
}
if ( !lead && !lim ) f[pos][pre][state]=res%mod;
return res;
}
int main()
{
int T; scanf( "%d",&T );
while (T--)
{
scanf( "%s",s );
int len=strlen(s); pos=0;
for ( int i=len-1; i>=0; i-- )
a[pos++]=s[i]-'0';
printf( "%lld\n",dfs( pos-1,0,0,1,1 )%mod );
}
return 0;
}
7——HDU4507 恨7不成妻
題意
如果一個整數滿足下列三個條件之一,稱為和7有關:
- 某一位是7
- 每一位的和是7的倍數
- 本身是7的倍數
求在區間 \([L,R]\) 中與7無關的數的平方和。\(\mod 1e9+7\)
思路
看上去很水,不過是稍微複雜和加強了一點。大體想法可以參考T5,第一個條件直接維護,第二個條件就記錄到當前位為止,數位和模 7 的餘數,第三個條件同 T5,記錄當前模 7的餘數即可。
……誒等等,你是不是忽略了什麼?平方和呢?
於是你不幸地發現,你還要再記錄一些東西:平方和,和,以及個數本身。為什麼呢?推個式子。
設當前位填的是 \(i\) ,後面 \(i-1\) 位子狀態得到的答案是 \(tmp\) (包含個數,一次方和,二次方和)
對於個數,顯然 \(res.cnt+=tmp.cnt;\)
對於一次方和 \(s_1\) ,\(res.s1+=tmp.s1+i\times 10^{pos-1}\times tmp.cnt\)
對於二次方和,設 \(tmp.s2=x_1^2+...+x_{cnt}^2\) ,每個數加上 \(add=i\times 10^{pos-1}\)
\[(x_1+add)^2+(x_2+add)^2+...+(x_{cnt}+add)^2\\\\ =(\sum x_{1\to cnt}^2)+2\times add\times (\sum x_{1\to cnt})+cnt\times add\\\\ =tmp.s2+2\times add\times tmp.s1+add^2\times tmp.cnt \]於是就可以 \(O(1)\) 計算了。細節部分見程式碼。
程式碼
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=21;
const ll mod=1e9+7;
struct node
{
ll cnt,s1,s2;
}f[N][7][7];
ll x[N],a[N],n;
node dfs( ll pos,bool lim,ll state,ll now)
//位,前面的位是否取滿,數位和,數本身
{
if ( pos==0 )
{
if ( now && state ) return (node){1,0,0};
else return (node){0,0,0};
}
if ( !lim && f[pos][state][now].s1 ) return f[pos][state][now];
//前面沒有卡滿(也就是每一位都取最大值),答案算過,那麼直接記憶化。
ll up=lim ? a[pos] : 9; //如果之前取了最大值,那麼只能 a[pos] ,否則隨便填都不會大於n
node res; res=(node){0,0,0};
for ( ll i=0; i<=up; i++ )
{
if ( i==7 ) continue;
ll st=(state+i)%7,sm=(now*10+i)%7;
node tmp=dfs( pos-1,(lim && i==up),st,sm );
ll add=i*x[pos-1]%mod;
(res.cnt+=tmp.cnt)%=mod;
(res.s1+=(tmp.s1+add*tmp.cnt%mod))%=mod;
(res.s2+=tmp.s2)%=mod; (res.s2+=2ll*tmp.s1*add%mod)%=mod;
(res.s2+=add*add%mod*tmp.cnt%mod)%=mod;
}
if ( !lim ) f[pos][state][now]=res;
return res;
}
ll solve( ll num )
{
n=0; memset( a,0,sizeof(a) );
for ( ; num; num/=10 )
a[++n]=num%10;
return dfs( n,1,0,0 ).s2;
}
int main()
{
ll T; scanf( "%lld",&T ); x[0]=1;
for ( ll i=1; i<=18; i++ )
x[i]=x[i-1]*10%mod;
while ( T-- )
{
ll l,r; scanf( "%lld%lld",&l,&r );
printf( "%lld\n",(solve(r)-solve(l-1)+mod)%mod );
}
return 0;
}
Last
To be continue....