學習筆記:數位dp
上講習題
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,2−fv,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(size−1,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+=fi−1,k,∣j−k∣≥2。注意,本題中如果有多個前導 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+=fi−1,x,mod(k−x),邊界條件 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給出(暫未更新)
相關文章
- 數位DP 學習筆記筆記
- 【學習筆記】數位DP筆記
- DP學習筆記筆記
- DP學習筆記(五)(2024.11.16)筆記
- DP學習筆記(四)(2024.10.2)筆記
- 插頭DP學習筆記筆記
- 數位DP小記
- (長期更新)DP 學習筆記筆記
- [筆記]數位dp例題及詳解(更新中)筆記
- [DP] 數位DP
- [學習筆記] 單調佇列最佳化DP - DP筆記佇列
- 演算法隨筆——數位DP演算法
- 高維字首和/SOS DP 學習筆記筆記
- 堆溢位學習筆記筆記
- 【學習筆記】數學筆記
- 數位 dp
- (長期更新)DP 最佳化 學習筆記筆記
- 【演算法學習筆記】概率與期望DP演算法筆記
- 動態dp複習筆記筆記
- TensorFlow常量、變數和佔位符詳解(學習筆記)變數筆記
- mysql修改表欄位學習筆記MySql筆記
- 組合數學學習筆記筆記
- 數學證明 學習筆記筆記
- 高等數學學習筆記(二)筆記
- 高等數學學習筆記(一)筆記
- 【學習筆記】組合數學筆記
- Python學習筆記 - 變數Python筆記變數
- 四元數 學習筆記筆記
- KLC 數點學習筆記筆記
- Solidity語言學習筆記————7、單位和全域性變數Solid筆記變數
- 數論學習筆記 (2):質數筆記
- 數位dp - 板子題
- C語言學習筆記——位運算C語言筆記
- 線性代數學習筆記(二)+貪心學習筆記(一)(2024.10.5)筆記
- #數位DP 計數問題
- Python學習筆記:過濾N位數並繪製折線圖Python筆記
- Python學習筆記 - 字串,數字Python筆記字串
- 集合冪級數學習筆記筆記