題解 DP專題彙總(15題)
DP專題
共15題
進度:15/15
完結撒花!!!
因為本人是蒟蒻所以好多題解都花了好長時間去理解
所以題解會從我自己思考的角度出發
並且 非 常 詳細地解釋
所以賊長(而且很囉嗦)QwQ
洛谷部落格傳送門
方便又快捷
\(Part1\) DP與樹
T1 GCD Counting (CF1101D)
T2 You Are Given a Tree (CF1039D)
T3 Vladislav and a Great Legend (CF1097G)
T4 Uniformly Branched Trees (CF724F)
\(Part2\) DP與計數
T5 Multiplicity (CF1061C)
T6 Maximum Element (CF886E)
T7 The Top Scorer (CF1096E)
\(Part3\) DP與狀壓
T8 Easy Problem (CF1096D)
T9 Kuro and Topological Parity (CF979E)
T10 Hero meet devil (HDU4899)
T11 Square (HDU5079)
T12 XHXJ's LIS (HDU4352)
\(Part4\) DP與雜題
T13 Delivery Club (CF875E)
T14 Make It One (CF1043F)
T15 New Year and Binary Tree Paths (CF750G)
Part 1 DP+樹
T1 GCD Counting (CF1101D)
資料加強版(UOJ) 100w資料
資料加強版(洛谷) 200w資料
題意簡述
給出一棵有n個節點的樹,樹有點權 \(a_i\) ,求樹上最長的一條鏈滿足:鏈上所有點的點權的 \(gcd>1\).
注意一個點也可以構成一條鏈.
資料範圍
\(1 \leq n \leq 2 \times 10^5\)
\(1 \leq a_i \leq 2 \times 10^5\)
\(1 \leq u,v \leq 2 \times n\) , \(u \neq v\)
題解
思路
首先看清題意,會發現一條鏈上的點權 \(gcd>1\)
於是乎就把難以處理的gcd轉換成了簡單的整除問題
從約數的角度考慮。假如我已經確定了一個 \(k\) ,那問題就變為了把樹上所有權值能被k整除的點 點亮,再找最長的鏈
顯然可以用樹上dp輕鬆搞定
但約數個數明顯也不少啊
所以不妨再退回來,一次解決所有k
再回到只觀察一個點,發現它只能由與它不互質的兒子轉移上來
那就只轉移這些就行了
再回來考慮 \(k\)
如果 \(k\) 不是質數,即 \(k=x*y\)
顯然 集合A{ \(i\) | \(k|a_i\) } \(\in\) 集合B{ \(i\) | \(x|a_i\) }
\(k\) 的答案當然沒有 \(x\) 的答案大啦
這樣就好辦了,分解質因數
做法
先把每個節點的質因數處理出來
$ 2 \times 3 \times 5 \times 7 \times 11 \times 13 < 200000 < 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17$
所以約數個數至多6個
用 \(f[N][7]\) 儲存約數即可
用 \(dp[i][j]\) 表示以 \(i\) 為根,經過 \(i\) 的,都能被 \(j\) 整除的最長路徑長度
轉移時直接暴力列舉即可(因為至多也就6*6=36嘛)
注意這只是從下往上的一條路徑,
所以還要邊dp邊更新答案
把約數個數看作常數就是 \(O(n)\)
注意事項
-
建雙邊別忘了開兩倍大小(也就只有我犯了這個錯吧)
-
預處理好像可以用篩加速
但是我懶所以直接打表(最大202ms)
不加速也可以吧4.5s耶
-
別忘了初始化dp陣列(全為1)
-
別忘了特判 \(a_i=0\) 的情況,輸出0
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const int N=20000005;
int n,a[N],f[N][7],dp[N][7],ans=1;
int k[100]={0,2,3,5,7,11,13,17,19,23,29,31,37,41,43,47,53,59,61,67,71,73,79,83,89,97,101,103,107,
109,113,127,131,137,139,149,151,157,163,167,173,179,181,191,193,197,199,211,223,
227,229,233,239,241,251,257,263,269,271,277,281,283,293,307,311,313,317,331,337,
347,349,353,359,367,373,379,383,389,397,401,409,419,421,431,433,439,443};
//sqrt(200000)內的質數表,打表使我快樂yeah
struct nod{
int to,nxt;
}e[N*2];//雙邊!
int head[N],cnt;
void add(int u,int v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
void dfs(int o,int fa){
for(int i=head[o];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
dfs(v,o);
for(int j=1;j<=f[o][0];j++)
for(int k=1;k<=f[v][0];k++)
if(f[o][j]==f[v][k])
ans=max(ans,dp[o][j]+dp[v][k]),//先更新答案
dp[o][j]=max(dp[o][j],dp[v][k]+1);//再更新dp
}
}
int main(){
scanf("%d",&n);
bool flag=1;
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
if(a[i]!=1) flag=0;//特判
}
for(int i=1;i<=n-1;i++){
int u,v; scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
if(flag) {printf("0");return 0;}//特判
for(int i=1;i<=n;i++)
for(int j=1;j<=86 && k[j]*k[j]<=a[i];j++)
if(a[i]%k[j]==0){
while(a[i]%k[j]==0) a[i]/=k[j];
f[i][++f[i][0]]=k[j];
}
//判斷每個數的能否被質數整除
for(int i=1;i<=n;i++)
if(a[i]!=1)
f[i][++f[i][0]]=a[i];
//除完肯定只剩下sqrt(200000)外的質數了,直接搬下來
for(int i=1;i<=n;i++)
for(int j=1;j<=f[i][0];j++)
dp[i][j]=1;
//初始化dp陣列
dfs(1,0);
printf("%d",ans);
}
時間測試
\(n=200000\) \(150ms\)
\(n=2000000\) \(1000ms\)
T2 You Are Given a Tree (CF1309D)
題意簡述
有一棵 \(n\) 個節點的樹
在樹上選擇一些長度為 \(k\) 的路徑,使得這些路徑覆蓋的點互不重複
求對於 \(k\) (\(1 \leq k \leq n\)) ,最多能選擇多少條路徑
資料範圍
\(1 \leq n \leq 10^5\)
\(1 \leq u \leq v \leq n\)
題解
思路
首先從特殊情況考慮
\(k=1 - n\) 想不到什麼好方法一下處理,先來對單個的k處理
覆蓋問題自然可以考慮貪心
一條鏈,必定可以找出一個點作為“根”(即 \(dep\) 最小的那個點)
對於一個節點 \(o\) ,想要覆蓋它
如果有一根長度為 \(k\) 的鏈以 \(o\) 為根,那自然把它數進來最好
不然就會佔用 \(o\) 上面的節點,顯然不優
(那如果這樣的鏈有很多條呢?
可 \(o\) 只能用一次啊!選哪種都是一樣的)
所以對於一個o來說,記錄轉移它子樹最長鏈與次長鏈的長度即可 \(O(n)\) 解決
一個 \(k\) 的情況搞定了,但 \(n\) 個 \(k\) 就要 \(O(n^2)\)
7s都無濟於事
所以不妨來觀察一下答案
顯然,\(ans \leq n/k\)
而且 \(ans\) 必然是單調不增的
對任意一個 \(a\) 來說,前 \(a\) 個 \(k\) ,答案種數不超過 \(a\) 種;後 \((n-a)\) 個 \(k\) ,答案種數不會超過 \(n/a+1\) 種 \((0 - n/a)\)
所以答案種數不會超過 \(a+n/a \geq 2 \times \sqrt n/a\) 種(當 \(a\) 取 \(\sqrt n\) 時)
對前 \(\sqrt n\) 個來說,暴力列舉即可,反正也沒多少個
對後 \(n-\sqrt n\) 個來說,就是列舉斷點
\(\Rightarrow\) 二分查詢
有多個斷點?
\(\Rightarrow\) 分治+二分
\(O(nlogn \sqrt n)\)
做法
首先,dp部分
用 \(f[i]\) 表示包括 \(i\)節點在內的,子樹下最長鏈長度
\(max1,max2\)表示不包括 \(i\) 節點在內的,子樹下最長鏈長度
然後轉移即可
如果找得到的話,注意 \(f[i]=0\) ,因為此節點不可再用了
分治+二分部分
用 \(l\) , \(r\) 表示查詢的區間
用 \(L\) , \(R\)表示查詢的值域
如果值域確定下來了表明這是個斷點,更新答案
否則dp一遍 \(mid\) 的答案,再去分治
更新答案的方向應該不用管,是分治嘛,無論哪個方向都會更新到的
注意事項
-
程式碼有點難以理解,慢慢想
-
max值的更新方式好像這樣是最正確也是最易於理解的
我原來寫的不知為何錯了
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,k,sum,flag;
int f[N],ans[N];
struct abc{
int to,nxt;
}e[2*N];
int head[N],cnt;
void add(int u,int v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
void dfs(int o,int fa){
int max1=0,max2=0;
for(int i=head[o];i;i=e[i].nxt){
int v=e[i].to;
if(v==fa) continue;
dfs(v,o);
if(f[v]>max1) max2=max1,max1=f[v];
else if(f[v]>max2) max2=f[v];
//這兩行是更新max值
}
if(max1+max2+1>=k) sum++,f[o]=0;//如果找得到
else f[o]=max1+1;//找不到就更新f[o]
}
void solve(int l,int r,int L,int R){
if(l>r || L>R) return;
if(L==R){
for(int i=l;i<=r;i++) ans[i]=L;
return;//記得啊
}
int mid=(l+r)>>1;
k=mid,sum=0;
dfs(1,0);
//跑一遍mid的答案
ans[mid]=sum;//別忘了更新ans[mid](因為下面不會列舉到這個點了)
solve(l,mid-1,sum,R);
solve(mid+1,r,L,sum);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n-1;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v),add(v,u);
}
for(int i=1;i<=sqrt(n);i++){
k=i;
sum=0;//sum為單次答案計數
dfs(1,0);
ans[i]=sum;
}
solve(sqrt(n)+1,n,0,sqrt(n));
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
}
T3 Vladislav and a Great Legend (CF1097G)
人生中第一道黑題
對我來說有點過於複雜與難以理解了,可能理解得不是很透徹
花了我一整天,我太弱了
題意簡述
給你一棵有 \(n\) 個節點的樹 \(T\),\(n\) 個節點編號為 \(1\) 到 \(n\) 。
對於 \(T\) 中每個非空的頂點的集合 \(X\) ,令 \(f(X)\) 為包含 \(X\) 中每個節點的最小連通子樹的最小邊數,即虛樹的大小。
再給你一個整數 \(k\) 。你需要計算對於每一個頂點的集合 \(X\) ,\((f(X))^{k}\) 之和,即:
\(\sum\limits_{X \subseteq\{1,2, \ldots, n\}, X \neq \varnothing}(f(X))^{k}\)
資料範圍
\(2 \leq n \leq 10^5\)
\(1 \leq u \leq v \leq n\)
$ 1 \leq k \leq 200$
題解
前鋪知識
樹形依賴揹包
[複雜版]{https://blog.csdn.net/wt_cnyali/article/details/76649037}
題意:在一棵有根樹上,每個節點都掛著一個物件有 \(w_i\) 的價值和 \(c_i\) 的體積
選出一個包含根節點的連通塊,使得體積之和不超過揹包大小 \(k\) ,價值之和最大。
[簡化版]{https://www.cnblogs.com/water-mi/p/9818622.html}
題意:給定一棵有 \(n\) 個節點的點權樹,要求你從中選出 \(m\) 個節點,使得這些選出的節點的點權和最大。
即每個節點的體積均為1
--下面是解法--
我們可以仿照揹包的思想
只不過在這裡不是一個整體的揹包,而是每個節點上都有一個揹包
轉移過程相當於把每個子節點當做一個物品,一個一個加進去
其他都與揹包一樣的
這道題用到的是樹形依賴揹包的思想
第二類斯特林數
[第二類斯特林數]{https://www.cnblogs.com/gzy-cjoier/p/8426987.html}
定義:第二類斯特林數 \(S(n,m)\) 表示的是把 \(n\) 個不同的小球放在 \(m\) 個相同的盒子裡方案數。
求法:\(S(n, m)=S(n-1, m-1)+m S(n-1, m)\)
考慮有 \(n-1\) 個小球,現在再放一個小球
如果新開一個,那就是 \(S(n-1, m-1) \Rightarrow S(n, m)\)
如果放到已有的盒子裡,有 \(m\) 種選擇,即 \(m S(n-1, m) \Rightarrow S(n, m)\)
性質:\(n^{k}=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(n, i)\)
\(n^k\) 相當於把 \(n\) 個不同的小球放到 \(k\) 個不同盒子裡,每個小球都有 \(k\) 種選擇
但這樣放會有一些盒子是空的,於是用 \(i\) 列舉有多少個盒子是非空的
確定到底是哪 \(i\) 個盒子是非空的,要乘上 \(C(n, i)\)
把k個不同的小球放入 \(i\) 個相同的盒子裡要 \(S(k, i)\)
但這裡盒子是不同的,因此再乘上 \(i!\)
思路
首先,看到要算的東西里有個大大的 \(k\) 次方
而且 \(k\) 還很小
可以考慮用第二類斯特林數的那個性質轉化
\(n^{k}=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(n, i)\)
於是
\(\sum(f(X))^{k}=\sum \sum\limits_{i=0}^{k} S(k, i) \times i ! \times C(f(X), i)\)
交換和號
\(=\sum\limits_{i=0}^{k} S(k, i) \times i ! \times \sum C(f(X), i)\)
噢!
發現其實左邊的東西都是可以很快算出來的
因此實際要算的是
\(\sum C(f(X), i)\)
於是我們把難以處理的冪換成了組合數
但這樣直接去考慮好像和冪沒有什麼區別
不妨從整體來考慮
考慮它的實際意義,其實就是 對於每個生成樹,從 \(f(X)\) 選出 \(i\) 條邊塗色的方案數之和
選出 \(i\) 條邊,像不像前面提到的樹形依賴揹包?
一個一個點來轉移
解決了?
做法
NO
這題真正的難點不在於思路,在於接下來的實現
仿照樹形依賴揹包,設 \(dp[i][j]\) 表示以 \(i\) 為根節點的子樹中,所有 點集形成的生成樹 中塗j條邊的方案數
請注意是 所有點集形成的生成樹 (即要求的)而不是 所有生成樹
請注意是所有而不是經過 \(i\) 節點的
我們是從邊的角度來考慮的
也就是說,存在一種特殊情況
只連一棵子樹(因為連了多棵子樹就合法了),不選根節點而選它連向子節點的邊
會被轉移,但不會被計入答案
\(why\)?
這種情況顯然是不合法的,又不選 \(o\) ,卻選了這條邊
但是我們稍後在上面轉移時需要它,因為是虛樹,在上面就不需要選中這個 \(o\) 點了
(這大概是最難理解的部分)
講個定義都已經把要點講了一半了
來個狀態轉移方程:
\(dp[o][k]=dp[o][i] \times dp[v][k-i] (0 \leq i \leq k)\)
很好理解,就是在前面的點裡先選 \(i\) 條塗色,再在這顆子樹裡選 \(k-i\) 條塗色
當然為了好寫實際上是這樣寫的
\(dp[o][i+j]=dp[o][i] \times dp[v][j] (0 \leq i \leq k, 0 \leq j \leq k-i)\)
理解到這可能又會冒出一個問題
這是按邊來轉移的,所以轉移到父節點時,又不 \(+1\) ,豈不是漏了 \(o\) 與 \(fa(o)\) 之間的邊了嗎?
當然其實可以連,也可以不連
所以dp完後還要把 \(dp[o][i]\) 變為 \(dp[o][i]+dp[o][i-1]\)
最後一個問題
如何初始化?
\(dp[o][0]=2\)
因為選了 \(o\) ,還是不選 \(o\) ,都是 \(0\) 條邊
所以有兩種情況
於是有一個東西不能轉移
就是初始化時 \(o\) 不選的情況,總不能轉移個空集吧
所以還得減 \(1\)
看起來是 \(O(nk^2)\) 的
其實是 \(O(nk)\) 的
複雜度證明我也搞不明白就不放上來了
注意事項
· 很難理解,請結合題解與程式碼,仔細思考,不要著急
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=1e5+5,mod=1e9+7;
struct nod{
long long to,nxt;
}e[2*N];
long long head[N],cnt;
void add(long long u,long long v){
e[++cnt].to=v;
e[cnt].nxt=head[u];
head[u]=cnt;
}
long long n,k;
long long dp[N][205],f[N],size[N],ans[N],S[205][205];
void dfs(long long o,long long fa){
size[o]=1,dp[o][0]=2;//初始化,dp[o][0]:選o還是不選o都是0條邊(後面會處理這種情況)
for(long long i=head[o];i;i=e[i].nxt){
long long v=e[i].to;
if(v==fa) continue;
dfs(v,o);
for(long long i=0;i<=k;i++) f[i]=0;
for(long long p=0;p<=min(k,size[o]);p++)
for(long long q=0;q<=min(k-p,size[v]);q++)
f[p+q]=(f[p+q]+(dp[o][p]*dp[v][q])%mod)%mod;
//類似揹包,f為臨時陣列避免混用
for(long long j=0;j<=k;j++) dp[o][j]=f[j];
//把答案從f轉移到dp[o]
for(long long j=0;j<=k;j++) ans[j]=(ans[j]+mod-dp[v][j])%mod;
//這一步其實可以放到後面,ans要減掉不經過o的情況,這裡先減了(為了好寫)
size[o]+=size[v];
}
for(long long i=0;i<=k;i++) ans[i]=(ans[i]+dp[o][i])%mod;
//累加答案,前面已經減過多餘的答案了
for(long long i=k;i>=1;i--) dp[o][i]=(dp[o][i]+dp[o][i-1])%mod;
//考慮o的父親,那麼顯然i條會變成i+1條,於是dp[o]整體往右移一格
dp[o][1]=(dp[o][1]+mod-1)%mod;
//減掉最前面不選o的情況(無法轉移至父節點)
}
int main(){
scanf("%lld%lld",&n,&k);
for(long long i=1;i<=n-1;i++){
long long u,v;
scanf("%lld%lld",&u,&v);
add(u,v),add(v,u);
}
dfs(1,0);
S[0][0]=1;
for(long long i=1;i<=k;i++)
for(long long j=1;j<=i;j++)
S[i][j]=(S[i-1][j-1]+(S[i-1][j]*j)%mod)%mod;
//計算第二類斯特林數
long long tmp=1,sum=0;
for(long long i=1;i<=k;i++) tmp=(tmp*i)%mod,sum=(sum+(tmp*S[k][i])%mod*ans[i])%mod;
//計算答案
printf("%lld",sum);
}
T4 Uniformly Branched Trees (CF724F)
人生中第二道黑題
又一道計數題,但相比T3要好理解得多
細節還是一如既往的多
題意簡述
求有多少種不同構(即交換節點不能使兩棵樹完全相同)的 \(n\) 個點構成的樹,滿足除了子節點(度數為1的結點)外,其餘結點的度數均為 \(d\) 。答案對質數 \(mod\) 取模。
資料範圍
\(1 \leq n \leq 10^3\)
\(2 \leq d \leq 10\)
\(10^8 \leq mod \leq 10^9\)
題解
思路
剛拿到這個題目,顯然最大的障礙就是如何處理同構(或者說如何判重)
對於一顆無根樹來說判重顯然是不現實的
自然就找一個根咯
這個根可不能隨便亂找,要有一些有用的性質才行
比如說重心
這樣一來子樹的大小就不會超過 \(\frac {n}{2}\) 了
那接下來怎麼做呢?
當然不是樹形dp因為連一顆固定的樹都沒有
但還是得dp
關聯的狀態是什麼呢?
顯然有 節點個數,子樹大小的上限
還有一個很妙的,根節點的子樹個數
這樣就方便轉移了
於是用 \(f[i][j][k]\) 表示 有i個節點,根節點有j個子節點,子樹大小均不超過k的方案總數
\(f[i][j][k]\) 中有兩種方案
一種是所有子樹 (指根節點的子樹,下同) 大小連等於 \(k\) 都沒有,全都比 \(k\) 小
直接轉移就行了
\(f[i][j][k]+=f[i][j][k-1]\)
第二種則是有子樹大小為 \(k\) 的
假設有 \(t\) 棵子樹大小為 \(k\) 的
那去掉這 \(t\) 棵,其實就是 \(f[i-t*j][j-t][k-1]\)
但從 \(f[i-t*j][j-t][k-1]\) 加上這些子樹的時候,其實又有很多種方案
每顆子樹有 \(f[t][d-1][k-1]\) 種
其實就是t棵子樹中,每顆子樹都可不分順序地,可重複地選 \(f[t][d-1][k-1]\) 種方案,即
\(C^{2}_{f[t][d-1][k-1]+t-1}\)
所以狀態轉移方程就出來了(別忘了前面的)
\(f[i][j][k]+=f[i][j][k-1]\)
\(f[i][j][k]+=f[i-t*j][j-t][k-1]+C^{2}_{f[t][d-1][k-1]+t-1} \times f[t][d-1][k-1]\)
最後,不要得意忘形o
還有雙重心要考慮(即n為偶數)
可以想象一下,就是一座橋連線著兩顆全等(對稱)的樹,橋的兩頭就是兩個重心
大概是這樣的
\ /
/\__/\
/ \
怎麼畫得跟個人似的[doge]
兩邊的方案自然是完全一樣的
\(f[n/2][d-1][n/2]\)
這時候就會發現一種重複了
比如有兩個方案 \(a\) 和 \(b\)
左 \(a\)右 \(b\) 和 左 \(b\) 右 \(a\) ,是沒有本質區別的(翻轉一下就一樣了)
當然 \(a!=b\) 不然是不會計算兩次的
要是這樣的話,直接減去 \(C^{2}_{f[n/2][d-1][n/2]}\)不就行啦~
做法
這個就很簡單了
直接按 \(i\),\(j\),\(k\),\(t\) 列舉轉移即可
注意初始化還有列舉的區間(寫在下面了QwQ)
組合數運算要除法,要用到逆元
只是階乘的逆元,看到質數用費馬小定理暴力處理就好了(反正不超過 \(d\) )
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=1005;
long long n,d,mod;
long long f[N][11][N/2];
long long inv[11];
long long ksm(long long a,long long x){
long long ans=1;
while(x){
if(x%2==1) ans=ans*a%mod;
a=a*a%mod;
x/=2;
}
return ans%mod;
}
void pre(){
inv[0]=1;
long long tmp=1;
for(long long i=1;i<=d;i++){
tmp=tmp*i;
inv[i]=ksm(tmp,mod-2);
}
}
long long C(long long m,long long n){
long long ans=1;
for(long long i=m;i>=m-n+1;i--) ans=(ans*i)%mod;
ans=(ans*inv[n])%mod;
return ans;
}
int main(){
cin>>n>>d>>mod;
if(n<=2) {cout<<1;return 0;}
pre();
for(long long i=0;i<=n/2;i++) f[1][0][i]=1;//初始化,只有一個節點就是1種情況
for(long long i=2;i<=n;i++){//列舉i
for(long long j=1;j<=min(i-1,d);j++){//列舉j
//j<=i-1(每棵子樹只有一個節點時最多也只有i-1棵子樹)
//j<=d(根節點至多有d棵子樹)
for(long long k=1;k<=n/2;k++){
f[i][j][k]=f[i][j][k-1];
for(long long t=1;t<=min((i-1)/k,j);t++){
//i-t*k>0 => t<=(i-1)/k
//t<=j(放入的新子樹數量t不會超過有的子樹數量j)
long long tmp=(k==1)?0:d-1;
//如果k=1,即只有一個根節點(無子樹),需要特判
f[i][j][k]=(f[i][j][k]+f[i-t*k][j-t][k-1]*C(f[k][tmp][k-1]+t-1,t)%mod)%mod;
}
}
}
}
long long ans=f[n][d][n/2];
if(n%2==0) ans=(ans+mod-C(f[n/2][d-1][n/2],2))%mod;//雙重心特判
cout<<ans;
}
Part 2 DP+計數
T5 Multiplicity (CF1061C)
很基礎的一個dp(為什麼是紫題)
題意簡述
從序列 \({a_1,a_2,...,a_n} (1 \leq ai \leq 1e6)\) 中選出非空子序列 \({b_1,b_2,...,b_k}\) ,一個子序列合法需要滿足,\(∀ i∈[1, k], i∣b_i\)
。求有多少互不相等的合法子序列,答案對
\(10^9+7\)取模。
注意:序列 \({1,1}\) 有 \(2\)種選法得到子序列 \(1\) ,但 \(1\) 的來源不同,認為這兩個子序列不相等。
資料範圍
\(1 \leq n \leq 10^5\)
題解
思路
顯然是dp
發現條件是跟選出來的數列有關的
所以狀態就要設計一個 選出來的數列長度
所以設 \(f[i][j]\) 表示從 \(a\) 數列前 \(i\) 個數中選出長度恰好為 \(j\) 的 \(b\) 數列個數
考慮從 \(f[i-1][]\) 轉移到 \(f[i][]\)
可以不選第 \(i\) 個,所以 \(f[i][j]+=f[i-1][j]\)
也可以選,這時候就要求 \(j|a[j]\)
所以狀態轉移方程
\(f[i][j]+=f[i-1][j]\)
\(f[i][j]+=f[i-1][j-1](j|a[j])\)
答案就是 \(\sum\limits_{i=1}^n f[n][i]\)
做法
\(n\) 是 \(10^5\) 級別的耶
所以要把 \(i\) 滾動掉
太暴力 \(n^2\) 肯定不行的
所以就只轉移 \(a[j]\) 的因子就行了
注意要從大到小轉移o(不然會影響到)
時間複雜度 \(O(n\sqrt{n})\)
注意事項
- 別忘了取模
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int mod=1e9+7;
int n,f[N],ans=0;
int main(){
scanf("%d",&n);
bool id=0;
f[0]=1;
for(int i=1;i<=n;i++){
int a;
scanf("%d",&a);
for(int j=max(a/n,1);j<=sqrt(a);j++){
if(a/j>n) continue;
if(a%j==0) f[a/j]=(f[a/j]+f[a/j-1])%mod;
}//列舉大於sqrt(n)的因數(從大到小)
for(int j=min(n,(int)sqrt(a));j>=1;j--){
if(a%j==0 && j*j!=a) f[j]=(f[j]+f[j-1])%mod;
}//列舉小於sqrt(n)的因數(從大到小)
}
for(int i=1;i<=n;i++) ans=(ans+f[i])%mod;//計算答案
printf("%d",ans);
}
T6 Maximum Element (CF886E)
又一道超水黑題
題意簡述
從前有一個叫 \(Petya\) 的神仙,嫌自己的序列求 \(max\) 太慢了,於是將序列求 \(max\) 的程式碼改成了下面這個樣子:
(太長了縮了一下QwQ)
int fast_max(int n,int a[]){
int ans=0,offset=0;
for(int i=0;i<n;++i)
if(ans<a[i]) ans=a[i],offset=0;
else{
offset++;
if(offset==k)return ans;
}
return ans;
}
廢話不多說,求有多少長度為 \(n\) 的排列,這個函式會返回錯誤的結果(即返回值不是n)。答案對 \(1e9+7\) 取模
資料範圍
\(1 \leq n,k \leq 10^6\)
不保證 \(n > k\)
題解
思路
直接考慮出錯的情況太難啦
不妨從反面考慮,正確的情況有多少
其實就是函式在 \(n\) 之前都沒有退出(只要到了 \(n\),返回值一定是正確的)
這樣就可以列舉 \(n\) 的位置來計算了
還差一個問題,選出一些數放到 \(n\) 的前面,怎麼計算不退出的情況數呢?
(這個問題只和元素間的相對大小有關,不妨把它們歸結到 \(i\) 的排列上來(是一樣的)(如果你看不懂上面這段話請當我什麼都沒說))
總而言之,是個很明顯的dp了
考慮 \(dp[i]\) ,表示 \(i\) 的排列中函式能夠執行完(即中途沒有退出) 的方案總數
考慮最大值 \(max\) (其實就是 \(i\) 但為了不混淆)的位置。顯然如果 \(max\) 在 \([1,i-k]\) 的話,最後 \(k\) 個數肯定都比 \(max\) 要小,那肯定就退出了(在最後一個退出也算退出)
反之,只要 \(max\) 在 \([i-k+1,i]\) 中,就只需要 \(max\) 的前面沒有退出就一定不會退出
所以用 \(j\) 來列舉 \(max\) 的位置
\(\sum\limits_{j=i-k+1}^i dp[j-1]\)
別忘了要先從i-1個數中選出j-1個數排到max前面 \(C^{j-1}_{i-1}\)
max後面的數是可以隨便放的 \(A^{i-j}_{i-j}=(i-j)!\)
$dp[i]=\sum\limits_{j=i-k+1}^i dp[j-1]C^{j-1}_{i-1} (i-j)! $
答案計算就很簡單了
\(ans=n!-\sum\limits_{i=1}^n dp[i-1]C^{n-i}_{n-1}(n-i)!\)
很遺憾 \(O(n^2)\) 過不去
我們來把這個式子開啟優化
\(dp[i]\)
$=\sum\limits_{j=i-k+1}^i dp[j-1] C^{j-1}_{i-1} (i-j)! $
\(=\sum\limits_{j=i-k+1}^i dp[j-1] \frac{(i-1)!}{(j-1)!(i-j)!}(i-j)!\)
\(=\sum\limits_{j=i-k+1}^i dp[j-1] \frac{(i-1)!}{(j-1)!}\)
\(=(i-1) !\sum\limits_{j=i-k+1}^i \frac{dp[j-1]}{(j-1)!}\)
\(=(i-1) !\sum\limits_{j=i-k}^{i-1} \frac{dp[j]}{j!}\)
很驚奇地發現 \(j\) 可以單獨拎出來!
於是字首和優化
\(O(n)\)
當然ans的計算也可以這樣優化(不過沒什麼必要只是為了好寫)
\(ans\)
\(=n!-\sum\limits_{i=1}^n dp[i-1]C^{n-i}_{n-1}(n-i)!\)
\(=n!-\sum\limits_{i=1}^n dp[i-1]\frac{(n-1)!}{(i-1)!}\)
做法
預處理階乘逆元什麼的
已經說得很清楚了吧?
注意事項
-
依舊是開long long
-
記得特判 \(n<=k\)
-
注意初始化和特判
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=1000005;
long long mod=1e9+7;
long long f[N],s[N],ans=0;
long long mul[N],inv[N];
long long ksm(long long a,long long x){
long long tot=1;
while(x){
if(x&1) tot=tot*a%mod;
a=a*a%mod,x>>=1;
}
return tot;
}
int main(){
long long n,k;
scanf("%lld%lld",&n,&k);
if(n<=k) {printf("0");return 0;}//特判
mul[1]=1;
for(long long i=2;i<=n;i++) mul[i]=mul[i-1]*i%mod;
inv[n]=ksm(mul[n],mod-2);
for(long long i=n-1;i>=1;i--) inv[i]=inv[i+1]*(i+1)%mod;//快速處理逆元
for(long long i=1;i<=k;i++) f[i]=mul[i],s[i]=(s[i-1]+f[i]*inv[i]%mod)%mod;
//當i<=k時,所有情況都不會退出
for(long long i=k+1;i<=n;i++){
f[i]=mul[i-1]*((s[i-1]+mod-s[i-k-1])%mod)%mod;//dp
s[i]=(s[i-1]+f[i]*inv[i]%mod)%mod;//(f[i]/i)字首和
}
ans=mul[n-1];//n=1時,就是(n-1)!種,不可不計算
for(long long i=2;i<=n;i++)
ans=(ans+f[i-1]*mul[n-1]%mod*inv[i-1]%mod)%mod;//計算答案
printf("%lld",(mul[n]+mod-ans)%mod);//取補集
}
T7 The Top Scorer (CF1096E)
題意簡述
小明在打比賽,包括小明自己共有 \(p\) 名選手參賽,每個人的得分是一個非負整數。最後的冠軍是得分最高的人,如果得分最高的人有多個,就等概率從這些人中選一個當冠軍。
現在小明已知自己的得分大於等於 \(r\) ,所有選手的得分和為 \(s\) 。所有人的得分情況隨機。求小明獲勝的概率,結果對 \(998244353\) 取模。
資料範圍
\(1 \leq p \leq 100\)
\(1 \leq r,s \leq 5000\)
題解
思路
首先來暴力dp
涉及到最大值,因此可以讓所有人都不超過某個值
設 \(dp[s][p][m]\) 表示 共有 \(p\) 人,總共 \(s\) 分,所有人的分都不超過 \(m\)的方案數
狀態轉移方程很好寫了,考慮新加入的是得分為i的選手
\(dp[s][p][m]=\sum\limits_{i=0}^m dp[s-i][p-1][m]\)
怎麼計算答案呢?
考慮到有多個人並列第一的情況,不妨列舉q人並列第一併且小明得t分,計算情況總數
當然這q個人裡必須包含小明
情況總數 \(tot=\sum\limits_{t=r}^s \sum\limits_{q=1}^p \frac{1}{q} C^{q-1}_{n-1} dp[s-q*t][n-q][t-1]\)
別忘了答案是求概率,還得除以總數
總數怎麼計算呢?
其實就是個數學問題,插板法
假設每個人的分數為 \(a_i(a_i \geq 0)\),小明分數為 \(k(k \geq r)\)
那麼就是求 \(a_1+a_2+...+a_{p-1}+k=s\) 的解集個數
轉化成正整數
\((a_1+1)+(a_2+1)+...+(a_{p-1}+1)+[k-(r-1)]=s+(p-1)-(r-1)\)
即求 \(b_1+b_2+...+b_p=s+p-r (b_i \geq 1)\) 的解集個數
就是插板法,在 \(s+p-r\) 個 \(1\) 間插入 \(p-1\) 個 \(+\) 號,分成 \(p\) 個數
即共 \(C_{s+p-r-1}^{p-1}\) 種方案
\(ans= \frac{tot} {C^{s+p-r-1}_{p-1}}\)
寫了這麼多,累死我了 該出答案了吧?
木有
會發現這是 \(O(rsp)\) 的,3s根本跑不過去
那麼接下來的事情就與dp無關了
看看 \(ans\) 那個式子,會發現其實我們只會用到 \(O(sp)\) 個數
所以能不能不經過遞推,直接把單個dp算出來呢?
可以。
\(dp[s][p][m]\) 其實就是把 \(s\) 個球投進 \(p\) 個籃子裡,使得每個籃子裡的球都不超過 \(m\) 個的方案數
怎麼去算呢?
直接算是不好操作的,“不超過 \(m\) 個” 不好處理
所以不妨來考慮容斥原理
先隨便投,\(C^{s+p-1}_{p-1}\)
然後來考慮至少有 \(i\) 個籃子裡超過 \(m\) 個球的情況數
首先要選出這 \(i\) 個籃子,\(C^i_p\)
這時候又可以來用插板法了,用 \(a\) 來表示這 \(i\)個選出的籃子,用 \(b\) 表示剩下的
\(a_1+a_2+...+a_i+b_1+b_2+..+b_{p-i}=s (a_i > m,b_i \geq 0)\)
\((a_1-m)+(a_2-m)+...+(a_i-m)+(b_1+1)+(b_2+1)+..+(b_{p-i}+1)\)
\(=s-im+(p-i)\)
\(=s-i(m+1)+p\)
記\(S_i=C^{p-1}_{s+p-i(m+1)-1}\)
剛開始選出來的 \(C^i_p\) 其實就是 \(S_0\)
最後來容斥,
\(dp[s][p][m]\)
\(=S_0-S_1+S_2-S_3+...+(-1)^mS_m\)
$ =\sum\limits_{i=0}^m (-1)^iS_i$
把 \(S_i\) 替換掉
\(dp[s][p][m]=\sum\limits_{i=0}^m (-1)^iC^{p-1}_{s+p-i(m+1)-1}\)
還有ans的演算法
\(tot=\sum\limits_{t=r}^s \sum\limits_{q=1}^p \frac{1}{q} C^{q-1}_{n-1} dp[s-q*t][n-q][t-1]\)
\(ans= \frac{tot} {C^{s+p-r-1}_{p-1}}\)
時間複雜度最大 \(O(p^2s)\)
做法
對於每一個要求的dp值,按照公式計算即可
組合數逆元啥的很基礎了
還需要解釋嗎?
注意事項
-
注意特判,尤其注意 \(\leq 0\) 的
-
long long...
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long P=110,N=5005;
long long mod=998244353;
long long mul[P+N],inv[P+N];
long long p,s,r,ans=0;
long long C(long long m,long long n){
if(m<0 || n<0 || m-n<0) return 0;//必要的,因為會有負數
if(m*n==0) return 1;//特判
return mul[m]*inv[n]%mod*inv[m-n]%mod;
}
long long ksm(long long a,long long x){
long long sum=1;
while(x){
if(x&1) sum=sum*a%mod;
a=a*a%mod,x>>=1;
}
return sum;
}
long long dp(long long s,long long p,long long m){
if(s==0 && p==0) return 1;//奇怪的特判
long long tot=0;
for(long long i=0;i<=p;i++){
long long tmp=C(p,i)*C(s+p-1-i*(m+1),p-1)%mod;
tot= (i%2)?(tot-tmp+mod)%mod:(tot+tmp)%mod;
}
return tot;
}
int main(){
scanf("%lld%lld%lld",&p,&s,&r);
mul[0]=inv[0]=1;
for(long long i=1;i<=p+s;i++) mul[i]=mul[i-1]*i%mod;
inv[p+s]=ksm(mul[p+s],mod-2);
for(long long i=p+s-1;i>=1;i--) inv[i]=inv[i+1]*(i+1)%mod;
//預處理階乘和逆元
for(long long t=r;t<=s;t++)
for(long long q=1;q<=p;q++)
ans=(ans+C(p-1,q-1)*ksm(q,mod-2)%mod*dp(s-q*t,p-q,t-1)%mod)%mod;
//公式計算答案
ans=(ans*ksm(C(s-r+p-1,p-1),mod-2))%mod;//最後要除以總數
printf("%lld",ans);
}
Part 3 DP+狀壓
T8 Easy Problem (CF1096D)
全村最水的題
題意簡述
給你一個長為 \(n\) 的字串以及 \(a_1...n\) ,刪去第 \(i\) 個字元的代價為 \(a_i\) ,你可以刪去一些字元,要求使得剩下的串中不含子序列 "hard",求最小代價。
(子序列不需要連續)
資料範圍
\(1 \leq n \leq 10^5\)
\(1 \leq a_i \leq 998244353\)
題解
思路
首先,顯然答案只跟 \(h a r d\) 有關
其它字元就不用刪也不用管了
接下來顯然考慮dp
既然是子序列,不要求連續,就只跟相對位置有關
設想掃一遍,掃的過程中,hard是由har加上d得到的,而har是由ha加上r得到的
那不妨就以已經有了hard中的前幾個字元為狀態吧
顯然是無後效性的
被動轉移即可
做法
設 \(f[i][j]\) 表示前 \(i\) 個字元中,剛好湊成 \(hard\) 中前 \(j\) 個字元最小的代價
\(i\) 可以滾動掉
當前為h:\(f[0]->f[1], f[0]->f[0]+w[i]\) (要想保持f[0],必須刪掉)
當前為a:\(f[1]->f[2], f[1]->f[1]+w[i]\)
當前為r:\(f[2]->f[3], f[2]->f[2]+w[i]\)
當前為d:\(f[3]->f[4], f[3]->f[3]+w[i]\)
\(O(n)\)
注意事項
-
答案是min(f[0],f[1],f[2],f[3])
-
開long long
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=100005;
long long n,f[5],w[N];
char s[N];
int main(){
scanf("%lld\n",&n);
for(long long i=1;i<=n;i++) scanf("%c",&s[i]);
for(long long i=1;i<=n;i++) scanf("%d",&w[i]);
for(long long i=1;i<=n;i++)
switch(s[i]){
case 'h':f[1]=min(f[0],f[1]),f[0]+=w[i];break;
case 'a':f[2]=min(f[1],f[2]),f[1]+=w[i];break;
case 'r':f[3]=min(f[2],f[3]),f[2]+=w[i];break;
case 'd':f[4]=min(f[3],f[4]),f[3]+=w[i];break;
}
printf("%lld",min(f[0],min(f[1],min(f[2],f[3]))));
}
T9 Kuro and Topological Parity (CF979E)
題意簡述
給定 \(n\) 個點,每個點有黑白兩種顏色(如果沒有顏色,那麼你可以把它任意塗成黑色或白色),同時你可以在這個圖上任意加入一些邊(當然不能加入重邊或自環),要求加入的邊必須從編號小的點指向編號大的點。
我們稱一條好的路徑為經過的點為黑白相間的路徑,如果一個圖好的路徑的總數 % $ 2= p $ ,那麼我們稱這個圖為好的圖,現在給定你 \(n\) 個點的情況,求這 \(n\) 個點能組成的好的圖的個數,答案取模 \(1e9+7\)
資料範圍
\(1 \leq n \leq 50\) $ (ex: 1 \leq n \leq 1e6) $
\(0 \leq p \leq 1\)
題解
這是 O(n) 的題解
這題在理解上有很多岔路,所以我會標出容易誤解的地方
為了方便,“好的路徑”基本都會用“路徑”來代替
題意
解釋一下,黑白相間指的是黑連向白,白連向黑
比如 \(1 \rightarrow 0 \rightarrow 1\),\(0 \rightarrow 1 \rightarrow 0 \rightarrow 1\),\(1\)
- (一個也算!)
思路
首先第一眼,是跟奇偶性相關的計數問題
所以重心就落在了奇偶性上
然後發現加入的邊必須從編號小的點指向編號大的點
顯然是無環的,而且拓撲序就是順序
直接dp好了
然而點也要選,邊也要選,可能性太多了qwq
那我們先不要管點,只考慮連邊
既然是按點dp,不妨來設想加入一個點,讓前面的點向它連邊
可是隨機性太大了啊,所以要加狀態來限制
首先能想到的就是顏色,要麼是黑,要麼是白
但這還不夠。因為我們想統計的是路徑數的奇偶性,而黑或白根本沒有體現
所以還要再加上兩種狀態:連到這個點的路徑總條數,要麼是奇,要麼是偶
綜合起來,就是四種狀態
\(ob\)(奇黑),\(ow\)(奇白),\(eb\)(偶黑),\(ew\)(偶白)
-
這四個是狀態,不是點的屬性!不是把點分為這四類,而是統計一個點有這些狀態時的方案數
-
奇偶性指的是好的路徑的總條數,而不是長度!
-
一個點不是隻能被一個前面的路徑連上,可以任意連!(甚至可以不連)而這些都加進方案數裡,我們要做的就是區分這些方案的奇偶性
那我們就來區分這些方案的奇偶性吧
假設我新的 \(i\) 號點是白色的,它已經連向了一些點了(或者沒有連)
考慮它再連向先前的另外一個點,顯然需要分情況討論
-
如果這個點是也白色的,那連向它也不會被算進好的路徑裡,不管連不連都不影響奇偶性
-
如果這個點是偶黑,連向它方案數等於加了一個偶數,不管連不連都不影響奇偶性
-
如果這個點是奇黑?
-
這個時候不能再單獨考慮了,需要結合整體考慮
-
如果 \(i\) 號點一共連向了奇數個奇黑,那奇偶性會改變(奇 \(\times\) 奇 \(=\) 奇)
-
如果 \(i\) 號點一共了連向了偶數個奇黑,那奇偶性就不會改變(奇 \(\times\) 偶 \(=\) 偶)
總結一下,如果不考慮奇黑,不管怎麼連都不會改變奇偶性,所以可以隨意連
那如果考慮奇黑?
其實有個很巧妙的想法,可以拿出一個奇黑來控制奇偶性
這個奇黑是可以選擇連或不連的,而這兩種情況奇偶性肯定不相同
舉個例子,比如其它 \(i-2\) 個點已經連好,路徑總數是奇條
如果果不連這個奇黑,奇的方案++
如果連這個奇黑,偶的方案++
好,那麼奇或偶的方案數就是各加上 \(2^{i-2}\)種
(因為選出一個奇黑控制奇偶性,其它隨便選,選或不選)
噢,我們還漏了一種情況
如果一個奇黑都沒有呢?
那奇偶性根本就改變不了了,只能是它自己一個點不連邊,奇的方案總數……
那它也就只能做奇白了,方案數加上 \(2^{i-1}\) 種
(不用選出一個奇黑控制奇偶性了,隨便選)
那 \(i\) 號點是黑的情況也能同理可得了
思路大概就是這樣了,或許不是很清晰?慢慢理解吧
做法
這才是精髓啊
根據上面的解釋,dp陣列的值肯定是方案數
但狀態呢?
再看一眼總結,會發現我們只關心有沒有奇黑(或奇白)
而其它的都很巧妙地被奇偶性繞開了
存狀態就只存這個!
設 \(f[i][id][ob][ow]\) 為前i個點裡,路徑總條數的奇偶性是偶或奇,有無奇黑,有無奇白的方案總數
而 \(O(n)\) 是怎麼做到的呢?
每次只轉移上一個
這也是為什麼狀態設計的是前 \(i\) 個點裡的總和,而不僅僅是第 \(i\) 個點
這樣轉移狀態的時候就不僅僅是 \(2^{i-1}\) 或 \(2^{i-2}\) 次方了,還要乘上 \(f[i-1][][][]\)
- 注意不是加,是乘
為什麼呢?
因為 \(f[i-1][][][]\) 意思是不包含 \(i\) 號點的方案數
而轉移計算的是包含 \(i\) 號點的方案數
兩種情況互不干擾,可以理解成先選不包含 \(i\) ,再選包含 \(i\) ,乘法原理
答案直接統計 \(f[n][p][][]\) 即可
\(O(n)\)
注意事項
-
初始化 \(f[0][0][0][0]=1\)
-
別忘 \(long long\) ……
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=1000000+5,mod=1e9+7;
long long n,p;
long long a[N],fx[N],f[N][2][2][2];
int main(){
scanf("%lld%lld",&n,&p);
for(long long i=1;i<=n;i++) scanf("%lld",&a[i]);
fx[0]=1;
for(long long i=1;i<=n;i++) fx[i]=fx[i-1]*2%mod;//預處理2^i
f[0][0][0][0]=1;//初始化
for(long long i=1;i<=n;i++)//列舉i
for(long long id=0;id<=1;id++)//列舉id
for(long long ob=0;ob<=1;ob++)//列舉ob
for(long long ow=0;ow<=1;ow++){//列舉ow
long long owo=f[i-1][id][ob][ow];//偷懶owo
if(a[i]!=0)//如果塗白
if(ob)//如果有奇黑
f[i][id][ob][ow]=(f[i][id][ob][ow]+fx[i-2]*owo%mod)%mod,//偶白
f[i][id^1][ob][1]=(f[i][id^1][ob][1]+fx[i-2]*owo%mod)%mod;//奇白(奇白就肯定為1了)
else
f[i][id^1][ob][1]=(f[i][id^1][ob][1]+fx[i-1]*owo%mod)%mod;//沒有奇黑,只能轉移到奇白
if(a[i]!=1)//如果塗黑
if(ow)//如果有奇白
f[i][id][ob][ow]=(f[i][id][ob][ow]+fx[i-2]*owo%mod)%mod,//偶黑
f[i][id^1][1][ow]=(f[i][id^1][1][ow]+fx[i-2]*owo%mod)%mod;//奇黑(奇黑就肯定為1了)
else
f[i][id^1][1][ow]=(f[i][id^1][1][ow]+fx[i-1]*owo%mod)%mod;//沒有奇白,只能轉移到奇黑
}
printf("%lld",(f[n][p][0][0]+f[n][p][0][1]+f[n][p][1][0]+f[n][p][1][1])%mod);
//直接計算答案
}
T10 Hero meet devil (HDU4899)
題意簡述
給出一個字串 \(S\) ,這個字串只由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 四個字母組成。
對於 \(0\)~\(|S|\) 中的每一個 \(i\) ,求出滿足以下條件的字串 \(T\) 的個數:
-
1.長度為 \(m\) ;
-
2.只由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 四個字母組成;
-
3.\(LCS(S,T)=i\)。
答案對 \(1e9+7\) 取模後輸出
- 延伸:對於長度為m的,隨機的,由 '\(A\)' , '\(C\)' , '\(G\)' , '\(T\)' 構成的字串,求它與 \(S\) 的 \(LCS\) 期望值
資料範圍
\(1 \leq T \leq 5\)
\(1 \leq |S| \leq 15\) , \(1 \leq n \leq 1000\)
\(Time: 1.2s\) , \(Memory:8MB\)
(其實可以跑進1s,原題沒這麼卡不過一樣的)
題解
思路
\(|S|\) 才 \(15\)……大概要用 \(2^n\) 的演算法
\(LCS\) 這個東西啊,其實並不是很好求……
可以先考慮一個字串 \(T\) 與 \(S\) 匹配
回想一下以前是怎麼求 \(LCS\) 的
設 \(dp[i][j]\) 為 \(S\) 的前 \(i\) 位與 \(T\) 的前 \(j\) 位匹配的 \(LCS\)
那麼有
\(dp[i][j]=dp[i-1][j-1]+1 (s[i]==t[j])\)
\(dp[i][j]=max(dp[i-1][j],dp[i][j-1]) (s[i]!=t[j])\)
這樣可以得到一個 \(dp\) 矩陣
那怎麼去計數呢?
回到多個 \(T\) 上
雖然 \(T\) 是不固定的,但是 \(S\) 是固定的
也就是說,不管 \(T\) 是多少,矩陣的行數總是 \(|S|\) 不變
當然我們只關心最後一列(即 \(dp[i][|T|]\))
那對於一個確定的 \(|T|\) 來說,只有最後一列是真正有用的,而它只有 \(|S|<=15\) 個數,每個數也不超過 \(15\)
所以最多也只有這麼多種狀態,很好統計
可以考慮以最後一列為狀態再進行一次dp,這次儲存的就是方案數了
可 \(15\) 個數的狀態有多少種呢……
先不說怎麼打了, \(15^{15}\) 肯定炸了吧
但是真的有這麼多種狀態嗎?
觀察發現,它肯定是單調不減的
而且相鄰兩位之間差絕對值不超過 \(1\)
因為多加一位, \(LCS\) 不可能多出2位啊
所以我們就可以狀壓了嘛,把差分陣列壓成二進位制
那這個大小就只有 \(2^{15}=32768\) ,好寫省時省空間~
做法
設 \(f[i][mac]\) 為長度為i的字串, \(LCS DP\)矩陣最後一列為mac的方案數
(這裡mac可以理解成15個數)
轉移就很簡單了,相當於在結尾加一個字元
等等,那加一個字元mac怎麼變呢?
顯然用前面 \(LCS\) 的樸素演算法就行了
預處理一個 \(trans[mac][i]\) 陣列,就很快了
預處理時間複雜度 $ O(|S|2^{|S|}) $
\(f[i+1][trans_{mac,k}]+=f[i][mac]\)
答案怎麼統計?
設 \(las_{mac}\) 表示 \(mac\) 的最後一個數(即\(dp[|S|][|T|]\))
\(ans[las_{mac}]+=f[m][mac]\)
時間複雜度 \(O(m2^{|S|})\)
總時間複雜度 \(O(T(|S|+m)2^{|S|})\),可以跑進 \(1s\)
注意事項
-
初始化 \(f[0][0]=1\)
-
這題卡空間,把 \(f\) 滾動一下即可
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int t,n,m;
char s[16];
int q[16],trans[33000][5],f[2][33000],ans[16];
void tran(int mac,int k){
int a[16]={},b[16]={};//記得清零
for(int i=1;i<=n;i++) a[i]=a[i-1]+((mac>>(i-1))&1);
//把狀壓差分開啟
for(int i=1;i<=n;i++){
if(k!=q[i]) b[i]=max(b[i-1],a[i]);
if(k==q[i]) b[i]=max(b[i],a[i-1]+1);
}//按照樸素LCS的轉移
for(int i=1;i<=n;i++) trans[mac][k]|=((b[i]-b[i-1])<<(i-1));
//把陣列化成狀壓差分
}
void pre(){
for(int j=0;j<=(1<<n)-1;j++)
for(int k=1;k<=4;k++)
tran(j,k);
}
int las(int mac){
int a[16]; a[0]=0;
for(int i=1;i<=n;i++) a[i]=a[i-1]+((mac>>(i-1))&1);
return a[n];
}
int main(){
scanf("%d",&t);
while(t--){
memset(trans,0,sizeof(trans));
memset(f,0,sizeof(f));
memset(ans,0,sizeof(ans));
//清空陣列
scanf("%s%d",s,&m);
n=strlen(s);
for(int i=1;i<=n;i++)
switch(s[i-1]){
case 'A':q[i]=1;break;
case 'C':q[i]=2;break;
case 'G':q[i]=3;break;
case 'T':q[i]=4;break;
}//把字元轉化成好處理的數字
pre();//預處理
bool id=1;//滾動
f[0][0]=1;//初始化(因為第一層id=1所以這裡是f[1][0]=1)
for(int i=1;i<=m;i++){
id=!id;
for(int j=0;j<=(1<<n)-1;j++) f[!id][j]=0;//先清零!
for(int j=0;j<=(1<<n)-1;j++)//列舉mac
for(int k=1;k<=4;k++)//列舉k
f[!id][trans[j][k]]=(f[!id][trans[j][k]]+f[id][j])%mod;
}
for(int j=0;j<=(1<<n)-1;j++)//列舉mac
ans[las(j)]=(ans[las(j)]+f[!id][j])%mod;
for(int i=0;i<=n;i++) printf("%d\n",ans[i]);
}
}
T11 Square (HDU5079)
題意簡述
給出一個 \(N \times N\) 的網格,其中有一些格子可以塗色,有一些格子已經損壞(即固定為黑色)。
定義一個網格的優美度為其中最大的白色子正方形邊長。
對於 \(1\) ~ \(n\) 中的每個 \(i\) ,求優美度為 \(i\) 的塗色方案數。
- 延伸:在能塗色的格子中任意塗色,求最大的白色子正方形邊長的期望值
資料範圍
\(1 \leq T \leq 10\)
\(1 \leq n \leq 8\)
題解
思路
跟T10很相似(還是有些不同)
首先n才8,大概又要考慮狀壓了
求最大正方形邊長是某個定值,肯定不如有大小關係的方便
於是記 \(ans[i]\) 為 最大白色子正方形邊長小於 \(i\) 的方案數(至於為什麼是小於等會就知道了)
於是只要輸出 \(ans[i+1]-ans[i] (0 \leq i \leq n)\) 即可
\(ans[0]=1\) (全塗黑)
\(ans[n+1]=2^{unbroken}\)(無論怎麼塗都可以)
所以只用考慮 \(i=2\) ~ \(n\)即可
先來想想最大白色子正方形邊長怎麼判定
還記得嗎?以前我們是按點用dp來判定的
但要是仿照T10,這個dp矩陣似乎有點過於龐大(超時)
所以乾脆來直接考慮邊
假設我們現在考慮的是邊長不超過 \(siz\) 的正方形
那一行裡就會有可能作為正方形底邊的 \(n-siz+1\) 條邊:
\(1\) ~ \(siz\) , \(2\) ~ \(siz+1\) , \(3\) ~ \(siz+2\) , \(\dots\) , \(n-siz+1\) ~ \(n\)
可以轉移的是什麼呢?
是從每條邊開始,最多能向上延伸k行
或者說從每條邊開始,能往上畫出一個 \(siz \times k\) 的白色矩形
或者說這個邊中的每一列向上延伸的連續正方形的最小值為k
(再不理解的去看看這個題解吧 QaQ)
這東西狀態就比較少了,但顯然不是二進位制的
顯然當 \(k>=siz\) 的時候,就已經形成了一個 \(siz \times siz\) 的白色正方形了
所以就要舍掉~
所以會發現每一位都是 \(0\) ~ \(siz-1\),也就是siz進位制的
所以剛開始的時候設的是小於 \(siz\) ,而不是小於等於。不然這裡每一位都是 \(1\) ~ \(siz\) ,不好處理了
做法
dp就好辦了,設 f[i][mac] 為第i行,狀態為mac的方案數
來暴力一點
從i-1行轉移到第i行的時候,先列舉mac( \(siz^{n-siz+1}\) )
再列舉第i行的塗色 ( \(2^n\) )
列舉 \(j\) ~ \(j+siz-1\) ,只要這裡面的塗色有黑格,那就意味著上面無論什麼正方形都被破壞掉了,\(new\)_\(mac\)的這一位變成 \(0\)
沒有黑格,那 \(new\)_\(mac\)的這一位就等於 \(mac\) 的這一位 \(+1\)
只要符合條件(沒有一位超過 \(siz\) ),就可以把 \(f[i-1][mac]\) 的值加進 \(f[i][new\)_\(mac]\)
外面還要套一層列舉 \(ans\) 的 \(n\) ,列舉 \(i\) 的 \(n\)
總時間複雜度 \(O(n^2 2^n siz^{n-siz+1})\)
於是還會發現,因為有些格子已被損壞,固定為黑格,其實不用列舉 \(2^n\) 個
不過不優化這個也能過啦
(值得一提,1s是有可能超的,但卡卡常還是挺快的)
注意事項
-
dp陣列開1100是絕對夠了,不放心還可加
-
ans陣列至少要開 \(10\) !(你還要放 \(n+1\) 的)
-
要計算的先計算出來再放到迴圈裡,不然慢啊……
-
dp時第一層只用dp一次就好了,初始化 \(f[0][0]=1\)
-
別忘清空哦,所有陣列都要清空!
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const int mod=1e9+7;
int t,n,siz;
bool a[10][10];
int f[10][1100],ans[10];
int ksm(int a,int x){int sum=1; while(x) {if(x&1) sum=sum*1LL*a%mod; a=a*1LL*a%mod,x>>=1;} return sum;}
//快速冪不解釋
void go(int r,int mac){
int m[9]={},tmp[9]={};
int q=mac;
for(int i=1;i<=n-siz+1;i++) m[i]=q%siz,q/=siz;//把mac拆開
for(int i=0;i<=(1<<n)-1;i++){//列舉格子塗色
int p[9]={},s[9]={}; bool flag=0;
for(int j=1;j<=n;j++){
p[j]=(i>>(j-1))&1;
if(p[j]==0 && a[r][j]==1){//只要有損壞的格子塗了白
flag=1;
break;
}
s[j]=s[j-1]+p[j];//字首和,方便統計是否有黑格
}
if(flag) continue;
for(int j=1;j<=n-siz+1;j++) tmp[j]=m[j];//tmp即新mac
for(int j=1;j<=n-siz+1;j++){
if(s[j+siz-1]-s[j-1]==0){//如果沒有黑格
tmp[j]++;
if(tmp[j]>=siz){
flag=1;
break;
}
}
else tmp[j]=0;//如果有黑格
}
if(flag) continue;
int new_mac=0;
for(int j=n-siz+1;j>=1;j--) new_mac=new_mac*siz+tmp[j];//把新mac壓好
f[r][new_mac]=(f[r][new_mac]+f[r-1][mac])%mod;//轉移
}
}
void solve(){
memset(f,0,sizeof(f));//清空f
f[0][0]=1;//初始化
int t=pow(siz,n-siz+1)-1;//計算的先拿出來算
go(1,0);//第一層一次就好
for(int i=2;i<=n;i++)
for(int j=0;j<=t;j++)
go(i,j);
for(int mac=0;mac<=t;mac++)
ans[siz]=(ans[siz]+f[n][mac])%mod;
}
int main(){
scanf("%d",&t);
while(t--){
memset(ans,0,sizeof(ans));
memset(a,0,sizeof(a));
scanf("%d",&n);
int tot=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++){
char c;cin>>c;
if(c=='*') a[i][j]=1;//a[i][j]表示該格是否已被損壞
else tot++;//統計unbroken個數
}
ans[1]=1,ans[n+1]=ksm(2,tot);
for(int i=2;i<=n;i++) siz=i,solve();
for(int i=0;i<=n;i++) printf("%d\n",(ans[i+1]+mod-ans[i])%mod);
}
}
T12 XHXJ's LIS (HDU4352)
經過前面幾題,想必大家對狀壓很熟悉了
題意簡述
求區間 \([L,R]\) 中,各位數字組成的序列的 \(LIS\) (最長上升子序列)恰好為 \(k\) 的數字個數。
- 延伸:在區間 \([L,R]\) 中任意選出一個數,求各位數字組成的序列的 \(LIS\) (最長上升子序列)的期望值。
資料範圍
\(1 \leq T \leq 10^4\)
\(0 < L \leq R <2^{63}-1\) , \(1 \leq k \leq 10\)
題解
思路
和前面幾題類似,還是考慮對於單個數如何求各位陣列成的 \(LIS\)
但是這裡不能用那個 \(O(n^2)\) 的演算法(用 \(f[i]\) 表示前 \(i\) 個的 \(LIS\) ),不但時間不優,而且空間不夠
有一種 \(O(nlogn)\) 的演算法。不記錄前 \(i\) 個的 \(LIS\) ,而是考慮用 \(f[i]\) 記錄 \(LIS\) 為 \(i\) 時最後一個數的最小值(只是為了方便理解,實際上無需記錄)
一個一個地加入,用 \(ans\) 表示目前的 \(LIS\)
如果加入的這個數是最大的,那麼 \(ans++\) ,\(f[ans]=a[i]\)
否則,在前面的 \(a[j](j<i)\) 中找到最小的,大於等於這個數的,把所有這樣的替換成 \(a[i]\) ,再更新一次 \(f\)
為什麼呢?
-
對於 \(i\) 前面的,既然 \(a[j]\) 已經是大於等於 \(a[i]\) 中最小的了,它變成 \(a[i]\) 不會改變大小關係,也不會影響答案。
-
對於 \(i\) 後面的,(如果不換)選用 \(a[i]\) 顯然比選用 \(a[j]\) 更優,替換掉相當於等效
舉個栗子,\(25413\)
-
加入2, 5,\(f[1]=2\),\(f[2]=5\)
-
加入4,4之前最小的 \(\geq4\) 的是5,因此把5換成4,24413,\(f[2]=4\)
-
加入1,1之前最小的 \(\geq1\) 的是2,因此把2換成1,\(14413\)
-
加入3,3之前最小的 \(\geq3\) 的是4,因此把4都換成3,\(13313\),\(f[2]=3\)
-
所以 \(LCS=2\)
當然光是這樣空間是 \(10^{10}\) ,想都不用想
其實這種dp有個特性,它只關心每個數有沒有出現過,卻不關心具體的位置與個數
於是我們可以直接記錄每個數是否出現過,空間只有 \(2^{10}=1024\)
最後的 \(LIS\) 即是出現的數的總個數了
這是狀壓部分,當然既然要求 \([L,R]\) 的,還得套個數位dp
發現狀壓和數位的加入順序都是從高位到低位,直接dp就好了
做法
設 \(f[i][mac]\) 為列舉到第i位,狀壓為mac時的方案數
不過詢問數有 \(10^4\) ,每次清零太慢了
不如再加一維 \(k\) 表示 \(LIS\) 為 \(k\) ,直接記錄不清零
數位dp我想不用再細說了吧
注意事項
-
這裡前導0是有影響的,注意判斷
-
狀態更新時注意如果原來是0,加入0是無需更新的(要特判)
AC程式碼
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll t,l,r,k;
ll a[22],f[22][1100][11];
ll dp(ll pos,ll mac,bool lead,bool eq){//數位dp模板
if(pos==0){//如果列舉完了
ll tot=0;
for(ll i=0;i<=9;i++) if((mac>>i)&1) tot++;
if(tot==k) return 1;//如果LIS為k
else return 0;
}
if(!eq && !lead && f[pos][mac][k]!=-1) return f[pos][mac][k];
ll ed=eq?a[pos]:9,ret=0;//模板
for(ll i=0;i<=ed;i++){
ll new_mac=mac;
if(!lead || i){//注意不為0或加上的數不為0才需要轉移
for(ll j=i;j<=9;j++)
if((mac>>j)&1){
new_mac^=(1<<j);
break;
}
new_mac|=(1<<i);
}
ret+=dp(pos-1,new_mac,lead && !i,eq && i==a[pos]);
}
if(!eq && !lead) f[pos][mac][k]=ret;
return ret;
}
ll solve(ll x){//數位dp模板
memset(a,0,sizeof(a));
while(x){
a[++a[0]]=x%10;
x/=10;
}
return dp(a[0],0,1,1);
}
int main(){
memset(f,-1,sizeof(f));//注意初始化
scanf("%lld",&t);
for(ll i=1;i<=t;i++){
scanf("%lld%lld%lld",&l,&r,&k);
printf("Case #%lld: %lld\n",i,solve(r)-solve(l-1));
}
}
Part 4 DP+雜題
T13 Delivery Club (CF875E)
這題就不是dp
題意簡述
有兩個快遞員,分別在 \(s_1\),$ s_2$,現在有 \(n\) 個任務,每個任務 \(x_i\) 表示要將貨物送到 \(x_i\) 。即讓任何一個快遞員到 \(x_i\) ,另一個快遞員原地不動。
由於快遞員之間需要有對講機聯絡,請你設計一種方案使得兩個快遞員之間的最長距離最短。
資料範圍
\(1 \leq n \leq 10^5\)
\(1 \leq s_1,s_2 \leq 10^9\)
題解
思路
看到這不禁想起了另一道題
有兩個人(A和B)和n個任務,兩個人完成每個任務的時間不同,但一個任務只需一個人完成。現在要求按順序完成,兩人可以同時完成不同的任務,求完成所有任務的最小時間。
\(1 \leq n \leq 200\) , \(1 \leq a_i,b_i \leq 200\)
兩個人就比較難處理
於是不難想到多開一維,記錄A的時間
設 \(f[i][j]\) 表示進行完第i個任務,A用了j的時間時,B用的時間最小值
答案即為 \(min(max(i,f[n][i]))\)
回到這道題,會發現一個比較難堪的事情:資料範圍大多了!
這種做法根本行不通的 所以不要dp了
換個思路,不如先二分答案
這樣問題就變為了判斷兩人最長距離是否能不超過 \(k\)
然後呢?正著來肯定要dfs了吧……
正著不行就反著來
送完第 \(i\) 件貨物,肯定有一個人在 \(a_i\) 的
現在我們關心另一個人的位置範圍,假設就是 \(l_i\) ~ \(r_i\) 吧
那 \(i-1\) 的情況肯定有一個人在 \(a_{i-1}\)
- 如果 \(a_{i-1}\) 正好就在 \(l_i\) ~ \(r_i\) 裡
那 \(i-1\) 的情況肯定有一個人在 \(a_{i-1}\),而另一個人的位置並沒有限制(他在哪都能走到 \(a_i\) )
所以 \(l_{i-1}=a_i-k\),\(r_{i-1}=a_i+k\)
- 如果 \(a_{i-1}\) 不在 \(l_i\) ~ \(r_i\) 裡呢?
那還是有一個人在 \(a_{i-1}\) 裡,
那另一個人又得在 \(a_{i-1}-k\) ~ \(a_{i-1}+k\) 裡,又得在 \(l_i\) ~ \(r_i\) 裡
\(l_{i-1}=max(a_{i-1}-k,l_i)\),\(r_{i-1}=min(a_{i-1}+k,r_i)\)
- 要是這兩部分根本就沒有重疊部分呢?
那 \(l_{i-1}>r_{i-1}\) 了, \(return\) 即可(其實不 \(return\) 也行,到最後也會 \(return\) 掉)
最後會得到 \(l_1\) ~ \(r_1\)
我們只需要一個人在這個範圍裡就行了
因為另一個人只要跟他距離不超過 \(k\) ,就肯定能來到 \(a_1\)
AC程式碼
#include<bits/stdc++.h>
using namespace std;
int n,s1,s2;
int a[100005];
bool check(int k){
int l=-1e9,r=1e9;
for(int i=n;i>=1;i--){
if(l<=a[i] && a[i]<=r) l=a[i]-k,r=a[i]+k;//a[i]在範圍內
else l=max(l,a[i]-k),r=min(r,a[i]+k);//a[i]不在範圍內
if(l>r) continue;
}
if((l<=s1 && s1<=r) || (l<=s2 && s2<=r)) return 1;//有一個在範圍內
return 0;
}
int main(){
scanf("%d%d%d",&n,&s1,&s2);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
int l=abs(s1-s2),r=1e9;//注意l不能賦成1(或者在上面判斷s1,s2的距離)
while(l!=r){
int mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
printf("%d",l);
}
T14 Make It One (CF1043F)
題意簡述
\(Shirley\) 有 \(n\) 個數,她可以選出一些數,使得它們的 \(gcd\) 為 \(1\) 。問最少能選多少個數?
如果任意選擇都不能滿足條件,請輸出 \(−1\)。
資料範圍
\(1 \leq n \leq 3*10^5\)
\(1 \leq a_i \leq 3*10^5\)
題解
思路
一眼看上去很簡單的樣子,卻又想不到什麼妙方法
特性很多,不如先來研究一下答案的特徵?
\(2 \times 3 \times 5 \times 7 \times 11 \times 13 < 300000 < 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17\)
答案最長只有7,因為只要不是無解,每加入一個數必定至少從gcd中去除一個質因數
最極限的情況:(轉自ZEZ題解)
\(a[1]= 3*5*7*11*13*17\)
\(a[2]=2 * 5*7*11*13*17\)
\(a[3]=2*3 * 7*11*13*17\)
\(a[4]=2*3*5 * 11*13*17\)
\(a[5]=2*3*5*7 * 13*17\)
\(a[6]=2*3*5*7*11 * 17\)
\(a[7]=2*3*5*7*11*13\)
於是我們列舉答案為 \(len\),判斷是否滿足
接下來怎麼辦呢?
一個很巧妙的想法:DP!
雖然只是要求我們判斷是否有滿足條件的解
但我們可以來計數(雖然看似多此一舉,但方便轉移)
設 \(dp[i]\) 表示選出 \(gcd\) 為 \(i\) 的方案數
最大公因數為 \(i\) 很不好處理,但有 \(i\) 這個因數就很好處理了
只需從能被i整除的數中選出 \(len\) 個即可
這其中還包含最大公因數不為 \(i\) 的,即都能被 \(i \times j\) 整除的
於是——
\(dp[i]=C^{len}_{cnt[i]}-\sum\limits_{j=2}dp[j]\)
其中 \(cnt[i]\) 表示 \(a[i]\) 中能被 \(i\) 整除的個數,可以預處理
顯然是從大往小dp
當然我們總不能從無窮大開始吧,要從 \(gcd\) 最大可能值—— \(a[i]\) 最大值開始
只要 \(dp[1]\) 不是 \(0\) ,就表示 \(len\) 可行
注意事項
- 做著做著你就會發現組合數太大了……
所以要模一個大質數
比如 \(988244353\) 什麼的
逆元直接打表噢
-
無需預處理
-
記得清空
AC程式碼
#include<bits/stdc++.h>
using namespace std;
const long long N=300005,mod=998244353;
long long n,maxer=-1e9;
long long a[N],cnt[N],dp[N];
long long fx[8]={0,1,499122177,332748118,249561089,598946612,166374059,142606336};
long long C(long long m,long long n){
if(m<n) return 0;
long long ans=1;
for(long long i=m;i>=m-n+1;i--) ans=ans*i%mod;
ans=ans*fx[n]%mod;
return ans;
}
int main(){
scanf("%lld",&n);
for(long long i=1;i<=n;i++){
scanf("%lld",&a[i]);
maxer=max(maxer,a[i]);
for(long long j=1;j<=sqrt(a[i]);j++)//預處理cnt
if(a[i]%j==0){
cnt[j]++;
if(a[i]!=j*j) cnt[a[i]/j]++;
}
}
for(long long len=1;len<=7;len++){//列舉答案
for(long long i=maxer;i>=1;i--){
dp[i]=C(cnt[i],len);
for(long long j=2;i*j<=maxer;j++)
dp[i]=(dp[i]-dp[i*j])%mod;//列舉轉移
}
if(dp[1]) {printf("%lld",len);return 0;}//如果有解就輸出
}
printf("-1");
}
T15 New Year and Binary Tree Paths (CF750G)
很妙的一道題,隱藏的數位dp
題意簡述
一顆無窮個節點的完全二叉樹,編號滿足線段樹分配,求有多少條樹上的簡單路徑編號和為 \(s\) 。
資料範圍
\(1 \leq s \leq 10^{15}\)
\(Time:3s\)
題解
思路
乍看沒有什麼思路……
仔細一想,一條路徑只有兩種可能,一條鏈或是一個分叉(頂點即為 \(LCA\) )
設頂點為鏈上深度最小的點。如果確定了頂點為 \(x\) ,其實有了 \(s\) 的限制,可能性也不多了
- 一條鏈
設鏈的長度為 \(h\)
假設從 \(x\) 往下全走左邊,權值和即為
\(\sum\limits_{i=0}^{h-1} 2^ix = (2^h-1)x \leq s\)
全走右邊呢?
\(\sum\limits_{i=0}^{h-1} 2^i(x+1)-1 = (2^h-1)(x+1)-h \geq s\)
只有唯一解 \(x_0\)
證明:
當 \(x=x_0+1\) 時,$ (2^h-1)x = (2^h-1)(x_0+1) = s+h >s $ ,與 \((1)\) 矛盾
當 \(x=x_0-1\) 時,$ (2^h-1)x_0 -h \leq s-h <s$,與 \((2)\) 矛盾
所以對於一個確定的深度 \(h\) ,頂點 \(x\) 是確定的!
但並不是每個 \(h\) 都有解
設現在鏈上的所有節點都在左邊,此時和為 \(base = (2^h-1)x\)
選出一些節點變成右邊
現在來考慮把一個左邊的結點變成右邊,這個點到鏈底的深度為 \(p\) ,其它相對於父節點位置不變
這樣一個變換會帶來 \(\sum\limits_{i=0}^{p-1} 2^ix = (2^p-1)x\) 的貢獻
只要找一些 \(p\) 使得貢獻和為 \(ret = s - base\) 即可
發現 \(2^i-1 = \sum\limits_{j=0}^{i-1} 2^j > \sum\limits_{j=0}^{j-1} 2^j-1\)
直接從大到小貪心做即可
\(ret = s\) % $ (2^h-1)$ , \((s>=2^h-1)\)
- 一個分叉 \(=\) 兩條鏈
類似的,設頂點為 \(x\)
但是為了方便,這次從子節點 \(2x\) (深度為 \(h_1\))與 \(2x+1\) (深度為 \(h_2\))開始算深度
還是先都往左走
\(base = x+2x(2^{h_1}-1)+(2x+1)(2^{h_2}-1)\)
$ = x+2^{h_1+1}x-2x+2^{h_2+1}x-2x+2^{h_2}-1$
\(=x(2^{h_1+1}+2^{h_2+1}-3)+2^{h_2}-1\)
同理仍然可以得到當 \(h_1\) , \(h_2\) 確定時,\(x\) 也是確定的
這次我們要用 \(2^1-1, 2^2-1,\dots,2^{h_1}-1,2^1-1,2^2-1,\dots,2^{h_2}-1\) 來拼湊出 \(s - base\)
顯然麻煩多了,因為湊出的方法有很多
來轉換一下,假設要選出 \(n\) 個數來湊,其實就是用 \(2^1, 2^2,\dots,2^{h_1},2^1,2^2,\dots,2^{h_2}\) 湊出 $ ret = s - base + n $
可以數位dp!
\(ret = (s-2^{h_2}+1)\) % $ (2^{h_1+1}+2^{h_2+1}-3)$ , \((s-2^{h_2}+1>=2^{h_1+1}+2^{h_2+1}-3)\)
做法
數位dp外面要列舉 \(h_1\) , \(h_2\) , \(n\)
這裡的數位dp其實並不是傳統的那種數位dp,只是借用了這個思想
順序也不是從高往低,而是從低往高
選第 \(i\) 個數( \(2^{i+1}\) )相當於往第 \(i+1\) 位加 \(1\) ,超過了 \(2\) 就會進位
所以設 \(f[i][j][k]\) 為選到第 \(i+1\) 位,共選了 \(j\) 個數,上一位是否進到了這一位
當然需要滿足 \((\)這一位選擇的數量\(+k)\)%\(2==ret\)的這一位
最後的答案是 \(f[log2(ret)][n][0]\) (因為進位就不是這個數了)
注意事項
這題細節也很多
-
計算2的冪不能直接用位運算(因為ll會溢位),需要預處理
-
預處理還需要多處理一位,因為後面會用到
-
計算 \(x\) 時要判斷 \(x\) 存不存在,不存在就 \(break\) 掉
-
算分叉時要特判 \(ret==0\) (不用轉移),直接加進答案
為什麼?我也不知道
-
只有當 \(ret\) 為偶數時才有解(程式碼中用 \(ret + n\) 代替了)
-
\(f\) 陣列初始化:\(f[0][0][0]=1\) ,記得清空
-
dp時要注意判斷能不能選
AC程式碼
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll s,bit[66],cnt,ans,f[66][155][2];
ll dp(ll s,ll tot,ll h1,ll h2){
memset(f,0,sizeof(f));
f[0][0][0]=1;//初始化
int ed=log2(s);
for(ll i=1;i<=ed;i++)
for(ll j=0;j<=i*2-2;j++)//j是之前選過的數的數量
for(ll k=0;k<=1;k++)//是否進位
for(ll p1=0;p1<=1;p1++){//選不選h1的
if(i>=h1 && p1) break;
for(ll p2=0;p2<=1;p2++){//選不選h2的
if(i>=h2 && p2) break;
if((p1+p2+k)%2==((s>>i)&1))//轉移條件
f[i][j+p1+p2][(p1+p2+k)/2]+=f[i-1][j][k];
}
}
return f[ed][tot][0];
}
int main(){
scanf("%lld",&s);
bit[0]=1;
for(;bit[cnt]<=s;)
cnt++,bit[cnt]=bit[cnt-1]*2;//預處理2的冪
bit[cnt+1]=bit[cnt]*2;//多處理一位
for(ll i=1;i<=cnt;i++){//鏈的情況
if(s<(bit[i]-1)) break;//x不存在
ll ret=s%(bit[i]-1);
for(ll j=i;j>=1;j--) if(ret>=bit[j]-1) ret-=bit[j]-1;//貪心
if(!ret) ans++;
}
for(ll h1=1;h1<=cnt;h1++)
for(ll h2=1;h2<=cnt;h2++){//列舉h1,h2
if((s-bit[h2]+1)<(bit[h1+1]+bit[h2+1]-3)) break;//x不存在
ll ret=(s-bit[h2]+1)%(bit[h1+1]+bit[h2+1]-3);
if(!ret) {ans++; continue;}//特判ret=0
for(ll n=1;n<=h1+h2;n++)//列舉n
if((ret+n)%2==0)//必須是偶數才有解
ans+=dp(ret+n,n,h1,h2);
}
printf("%lld",ans);
}