學習筆記:數位dp

心情想要飛發表於2020-10-06

上講習題

AcWing 1075

本題每個數都可以變成下一個點,很像某個點向下一個點連邊。一個數只有一個因子和,但是有可能多個數的因子和等於某個數,就像是一個點只有一個爹,但是會有多個兒子,所以這樣建圖是一棵樹,每個點的爹就是自己的因子和。任意一個序列都對應樹的一條路徑,要求最長的序列就是求樹的直徑。求樹的直徑參考上講AcWing 1072

#include<bits/stdc++.h>
using namespace std;
const int NN=50004;
int sum[NN],ans;
vector<int>g[NN];
int dfs(int u)
{
    int maxx=0,maxy=0;
    for(int i=0;i<g[u].size();i++)
    {
        int res=dfs(g[u][i])+1;
        if(res>maxx)
        {
            maxy=maxx;
            maxx=res;
        }
        else if(res>maxy)
            maxy=res;
    }
    ans=max(ans,maxx+maxy);
    return maxx;
}
int main()
{
    int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=2;j<=n/i;j++)
			sum[i*j]+=i;
    for(int i=1;i<=n;i++)
        if(sum[i]<i)
            g[sum[i]].push_back(i);
    dfs(1);
	printf("%d",ans);
	return 0;
}

AcWing 1074

本題要求剪掉一些枝,可以分別給左右兒子分配一些保留的枝並加上自己的枝上的蘋果。先預處理出所有的點的左右兒子,然後把連向父親的邊的蘋果放在自己身上。注意,因為根節點沒有父親,然而留下的點會算它一個,所以留下的邊數要加一。本題的狀態有重複,需要記憶化。

#include<bits/stdc++.h>
using namespace std;
const int NN=104;
int a[NN],g[NN][NN],l[NN],r[NN],f[NN][NN],n,m;
void dfs(int u)
{
	for(int i=1;i<=n;i++)
		if(g[u][i]>=0)
		{
			l[u]=i;
			a[i]=g[u][i];
			g[u][i]=g[i][u]=-1;
			dfs(i);
			break;
		}
	for(int i=1;i<=n;i++)
		if(g[u][i]>=0)
		{
			r[u]=i;
			a[i]=g[u][i];
			g[u][i]=g[i][u]=-1;
			dfs(i);
			break;
		}
}
int dp(int u,int x)
{
    int &d=f[u][x];
	if(d>=0)
	    return d;
	if(!x)
		return d=0;
	if(!l[u]&&!r[u])
		return d=a[u];
	for(int k=0;k<x;k++)
		d=max(d,dp(l[u],k)+dp(r[u],x-k-1)+a[u]);
	return d;
}
int main()
{
	scanf("%d%d",&n,&m);
	m++;
	memset(g,0xaf,sizeof(g));
	for(int i=1;i<n;i++)
	{
		int u,v,w;
		scanf("%d%d%d",&u,&v,&w);
		g[u][v]=g[v][u]=w;
	}
	dfs(1);
	memset(f,-1,sizeof(f));
	printf("%d",dp(1,m));
	return 0;
}

AcWing 1077

本題和上講AcWing 323非常像。但是我們研究一下發現,假設某個點不用,則所有兒子至少用一個。但是這種分析會少考慮一個情況:有可能父親用了,那麼兒子也可以一個都不用。於是我們就要分成三種狀態: f u , ( 0 , 1 , 2 ) f_{u,(0,1,2)} fu,(0,1,2),分別表示父親一定用了且自己一定沒用、一定有一個兒子用了且自己一定沒用、自己一定用了的情況下覆蓋完本子樹的最小代價。首先,第一種情況,那麼自己的每個兒子可以選擇用或者不用,而且因為自己一定不用,所以子節點不能選擇父親一定用了的情況, f u , 0 + = min ⁡ ( f v , 1 , f v , 2 ) f_{u,0}+=\min(f_{v,1},f_{v,2}) fu,0+=min(fv,1,fv,2)。第二種情況,則所有子節點要至少有一種,同理,子節點也不能選擇父親一定用的情況, f u , 1 = min ⁡ ( f v , 1 , f v , 2 ) f_{u,1}=\min(f_{v,1},f_{v,2}) fu,1=min(fv,1,fv,2)。但是這種情況如果更小的全是 f v , 1 f_{v,1} fv,1,則要找到一個變了之後差值最小的替換,即 m i n n = min ⁡ ( f v , 2 − f v , 1 ) minn=\min(f_{v,2}-f_{v,1}) minn=min(fv,2fv,1)。如果每個都是用 f v , 2 f_{v,2} fv,2更新的,那麼 f u , 1 f_{u,1} fu,1就要加上 m i n n minn minn。考慮簡化這個式子,發現如果用的 f v , 2 f_{v,2} fv,2更新,那麼 f v , 2 = min ⁡ ( f v , 1 , f v , 2 ) f_{v,2}=\min(f_{v,1},f_{v,2}) fv,2=min(fv,1,fv,2),帶入剛才求 m i n n minn minn的式子剛好等於 0 0 0。遇到 0 0 0相當於不用加了,和想要的效果剛好相對應,則不用判斷是否用了 f v , 2 f_{v,2} fv,2直接更新即可。第三種情況,則孩子可以用或者不用,則 f u , 2 = min ⁡ ( f v , ( 0 , 1 , 2 ) ) f_{u,2}=\min(f_{v,(0,1,2)}) fu,2=min(fv,(0,1,2))。注意別忘了加上使用自己的代價。有一個問題:如何找根?方法很簡單:找入度為零的即可。最後輸出答案時根節點沒有父親,所以 a n s = min ⁡ ( f r o o t , 1 , f r o o t , 2 ) ans=\min(f_{root,1},f_{root,2}) ans=min(froot,1,froot,2)

#include<bits/stdc++.h>
using namespace std;
const int NN=2504;
struct node
{
	int num,son[NN],money;
}a[NN];
int f[NN][3];
bool isson[NN];
int dp(int x,int fa)
{
	int minn=2147483647;
	f[x][2]=a[x].money;
	for(int i=1;i<=a[x].num;i++)
	{
		int y=a[x].son[i];
		dp(y,x);
		f[x][0]+=min(f[y][1],f[y][2]);
		f[x][1]+=min(f[y][1],f[y][2]);
		f[x][2]+=min(f[y][2],min(f[y][1],f[y][0]));
		minn=min(minn,f[y][2]-min(f[y][1],f[y][2]));
	}
	f[x][1]+=minn;
}
int main()
{
	int n,root=1;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		int x;
		scanf("%d",&x);
		scanf("%d%d",&a[x].money,&a[x].num);
		for(int j=1;j<=a[x].num;j++)
		{
			scanf("%d",&a[x].son[j]);
			isson[a[x].son[j]]=true;
		}
	}
	while(isson[root])
		root++;
	dp(root,0);
	printf("%d",min(f[root][1],f[root][2]));
	return 0;
}

概念

數位 d p dp dp,就是一個構造數的 d p dp dp。這類問題一般求滿足要求的數的個數。

方法

一般來說都是研究上邊界這個數的每一位,從最高位開始。如果這一位填的是上邊界,則要繼續判斷;如果填的不是上邊界,則後面的可以隨便填,直接計算並退出即可。因為要算隨便填的方案數,所以可以初始化某一位開始,隨便填且滿足題目要求的方案數。

例題

AcWing 1081

不難發現,如果一個數填的不是 1 1 1 0 0 0,那麼就需要重複的數相加,一定不滿足要求。則本題就是求把一個數拆成 b b b進位制,每一位填 1 1 1 0 0 0,剛好填 k k k 1 1 1且數不超過 y y y的方案數。如果上界大於 0 0 0,那麼這一位填 0 0 0後面的就可以隨便填且要選 k k k位填 1 1 1 a n s + = C ( s i z e , k ) ans+=C(size,k) ans+=C(size,k)。如果上界還大於 1 1 1,那麼這一位填 1 1 1後面也可以隨便填,則 a n s + = C ( s i z e − 1 , k ) ans+=C(size-1,k) ans+=C(size1,k),而且這一位不管填什麼後面都隨便填,那麼已經把所有方案計算了,直接退出。如果上界這一位等於 1 1 1,那麼這一位填了 1 1 1其他的就不能亂填,可是需要填的就少了,記 l a s t last last為前面填 1 1 1的個數,則 l a s t + + last++ last++,前面計算 C C C也要減去這些固定的。若上界等於 0 0 0,那麼填了 0 0 0後也不能亂填,不能對答案有貢獻。最後全部的都算完了後若已經固定了 k k k的每一位,即 l a s t = k last=k last=k,則答案 + 1 +1 +1。本題中,初始化從某一位開始隨便填的方案數,就是初始化 C C C的值。

#include<bits/stdc++.h>
using namespace std;
const int NN=33;
int C[NN][NN],k,b;
int dp(int n)
{
    vector<int>num;
    while(n)
    {
        num.push_back(n%b);
        n/=b;
    }
    int res=0,last=0;
    for(int i=num.size()-1;i>=0;i--)
    {
        int x=num[i];
        if(x)
        {
            res+=C[i][k-last];
            if(x>1)
            {
                res+=C[i][k-last-1];
                break;
            }
            else
            {
                last++;
                if(last>k)
                    break;
            }
        }
        if(!i&&last==k)
            res++;
    }
    return res;
}
int main()
{
    for(int i=0;i<NN;i++)
        for(int j=0;j<=i;j++)
            if(!j)
                C[i][j]=1;
            else
                C[i][j]=C[i-1][j]+C[i-1][j-1];
    int l,r;
    scanf("%d%d%d%d",&l,&r,&k,&b);
    printf("%d",dp(r)-dp(l-1));
    return 0;
}

AcWing 1083

這個題每一位只要不是當前位的上界就可以隨便填。但是題目要求兩個數的差必須大於 2 2 2,則看一看上一位的邊界與這一位填的數的差是否大於 2 2 2即可。如果在列舉當前位填邊界的情況時,發現兩位的邊界的差小於 2 2 2,則這樣填這個序列已經不滿足要求了,直接退出即可。最後考慮後面隨便填的方案數,設 f i , j f_{i,j} fi,j為有 i i i位且最高位是 j j j的數的個數。兩位的差大於二即可轉移,則 f i , j + = f i − 1 , k , ∣ j − k ∣ ≥ 2 f_{i,j}+=f_{i-1,k},|j-k|\ge2 fi,j+=fi1,k,jk2。注意,本題中如果有多個前導 0 0 0是會計算為不可取的方案,因為有兩位都是 0 0 0 ,差小於 2 2 2。但是如果有多個前導 0 0 0,在本題中應當是可取的,所以要特殊判斷。有多個前導 0 0 0相當於前幾位都固定為 0 0 0,後面的隨便填,因為最高位一定大於 0 0 0。最後,如果列舉完了,那麼說明都填最高位是可以的,答案加一。注意, 0 0 0也是一個可行的數。

#include<bits/stdc++.h>
using namespace std;
const int NN=11;
int f[NN][NN];
int dp(int n)
{
    if(!n)
        return 1;
    vector<int>num;
    while(n)
    {
        num.push_back(n%10);
        n/=10;
    }
    int res=0,last=-2;
    for(int i=num.size()-1;i>=0;i--)
    {
        int x=num[i];
        for(int j=i==num.size()-1;j<x;j++)
            if(abs(j-last)>=2)
                res+=f[i+1][j];
        if(abs(x-last)<2)
            break;
        last=x;
        if(!i)
            res++;
    }
    for(int i=1;i<num.size();i++)
        for(int j=1;j<=9;j++)
            res+=f[i][j];
    return res+1;
}
int main()
{
    for(int i=1;i<NN;i++)
        for(int j=0;j<=9;j++)
            if(i==1)
                f[i][j]=1;
            else
                for(int k=0;k<=9;k++)
                    if(abs(j-k)>=2)
                        f[i][j]+=f[i-1][k];
    int l,r;
    scanf("%d%d",&l,&r);
    printf("%d",dp(r)-dp(l-1));
    return 0;
}

AcWing 1084

本題是同樣的思路,如果等於邊界就繼續列舉,反之就加上後面隨便填的總方案數。考慮隨便填的方案數,發現要求前面填邊界的數的總和加上後面填的數的綜合模 n n n 0 0 0,所以可以把 l a s t last last設為前面填的邊界的總和,並設 f i , j , k f_{i,j,k} fi,j,k表示有 i i i位,且最高位為 j j j,模 n n n k k k的數的個數。則每次 r e s + = f i , j , − l a s t res+=f_{i,j,-last} res+=fi,j,last,因為後面模數填夠 − l a s t -last last,兩個模數相加就是 0 0 0了。考慮狀態轉移,列舉數的第二位 x x x,則 f i , j , k + = f i − 1 , x , m o d ( k − x ) f_{i,j,k}+=f_{i-1,x,mod(k-x)} fi,j,k+=fi1,x,mod(kx),邊界條件 f 1 , j , m o d ( j ) = 1 f_{1,j,mod(j)}=1 f1,j,mod(j)=1

#include<bits/stdc++.h>
using namespace std;
const int NN=11;
int f[NN][NN][104],P;
int mod(int x)
{
    return (x%P+P)%P;
}
int dp(int n)
{
    if(!n)
        return 1;
    vector<int>num;
    while(n)
    {
        num.push_back(n%10);
        n/=10;
    }
    int res=0,last=0;
    for(int i=num.size()-1;i>=0;i--)
    {
        int x=num[i];
        for(int j=0;j<x;j++)
            res+=f[i+1][j][mod(-last)];
        last+=x;
        if(!i&&!(last%P))
            res++;
    }
    return res;
}
int main()
{
    int l,r;
    while(scanf("%d%d%d",&l,&r,&P)!=EOF)
    {
        memset(f,0,sizeof(f));
        for(int i=0;i<=9;i++)
            f[1][i][mod(i)]=1;
        for(int i=2;i<NN;i++)
            for(int j=0;j<=9;j++)
                for(int k=0;k<P;k++)
                    for(int x=0;x<=9;x++)
                        f[i][j][k]+=f[i-1][x][mod(k-j)];
        printf("%d\n",dp(r)-dp(l-1));
    }
    return 0;
}

習題

AcWing 1082

AcWing 1085

AcWing 1086

解析和程式碼在下一篇部落格——單調佇列優化 d p dp dp給出(暫未更新)

相關文章