1. 程式人生 > 實用技巧 >數位DP複習筆記

數位DP複習筆記

前言

複習筆記第五篇。(由於某些原因(見下),放到了第六篇後面更新)CSP-S RP++.

luogu 的難度評級完全不對,所以換了順序,換了別的題目。有點亂,見諒。要罵就罵洛谷吧,原因在T2處

注:此文大部分建議結合程式碼閱讀,便於解釋

0——HDU 3555 Bomb

link

題意

\(1\sim N\) 中包含子串 49 的個數。\(N\leq 2^{63}-1.\)

思路

\(f[i][0]\) 表示長度為 \(i\) ,不含有 49 的個數;\(f[i][1]\) 為最高位為 9 但不含 49 的個數;\(f[i][2]\) 表示含有 49 的個數。轉移方程為:

\[f[i][0]=10\times f[i-1][0]-f[i-1][1]\\\\ f[i][1]=f[i-1][0]\\\\ f[i][2]=10\times f[i-1][2]+f[i-1][1] \]

對於每一個位數,預處理出 \(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]數字計數

link

題意

給定兩個正整數 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 煩人的數學作業

link

感謝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 花神的數論題

link

題意

\(\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

link

題意

\(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

link

題意

當一個數字,從左到右依次看過去數字沒有出現先遞增接著遞減的“山峰”現象,就被稱作 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不成妻

link

題意

如果一個整數滿足下列三個條件之一,稱為和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....