簡單普通 DP
*I. P3643 [APIO2016]划艇
題意簡述:給出序列 \(a_i,b_i\),求出有多少序列 \(c_i\) 滿足 \(c_i=-1\) 或 \(c_i\in[a_i,b_i]\),同時非 \(-1\) 的部分單調遞增。
直到做到這題我才意識到我的 DP 水平有多菜。
注意到值域過大,於是對區間進行離散化,設離散化後的端點分別為 \(p_1,p_2,\cdots,p_c\)。注意要將 \([a_i,b_i]\) 變成 \([a_i,b_i+1)\),即將 \(b_i\) 加 \(1\),方便計算答案。
接下來考慮 DP:設 \(f_{i,j}\) 表示第 \(i\) 個學校派出划艇數量在 \(L_j=[p_j,p_{j+1})\)
錯誤思路:\(f_{i,j}=\begin{cases}\sum_{pos=1}^{i-1}\sum_{k=1}^{j-1}f_{pos,k}\times (p_{j+1}-p_j)&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)。錯誤原因:I. 沒有考慮邊界條件 & 列舉下界。II. 沒有考慮在同一區間內也合法的情況。
邊界條件就是 \(f_{i,0}=1\),並且注意 \(pos,k\) 的列舉下界應為 \(0\)。
考慮在同一區間內合法的情況:不妨列舉最右端的不在區間 \(j\)
綜上所述,更新後的轉移方程應為 \(f_{i,j}=\begin{cases}\sum_{pos=0}^{i-1}\sum_{k=0}^{j-1}f_{pos,k}\times\binom{m+L_j-1}{m}&[p_j,p_{j+1})\subseteq[a_i,b_i)\\0&\mathrm{otherwise}\end{cases}\)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N=500+5;
const int mod=1e9+7;
ll n,cnt,a[N],b[N],p[N<<1];
ll g[N],iv[N];
int main(){
cin>>n,g[0]=1;
for(int i=1;i<=n;i++){
cin>>a[i]>>b[i],iv[i]=(i==1?1:-mod/i*iv[mod%i]%mod+mod);
p[++cnt]=a[i],p[++cnt]=b[i]+1;
} sort(p+1,p+cnt+1),cnt=unique(p+1,p+cnt+1)-p-1;
for(int i=1;i<=n;i++){
a[i]=lower_bound(p+1,p+cnt+1,a[i])-p;
b[i]=lower_bound(p+1,p+cnt+1,b[i]+1)-p;
} for(int i=1;i<cnt;i++){
ll len=p[i+1]-p[i];
for(int j=n;j;j--)
if(a[j]<=i&&i<b[j]){
ll f=0,m=0,C=1;
for(int k=j;k;k--){
if(a[k]<=i&&i<b[k])C=C*(m+len)%mod*iv[m+1]%mod,m++;
f=(f+g[k-1]*C)%mod;
} g[j]=(g[j]+f)%mod;
}
} ll ans=0;
for(int i=1;i<=n;i++)ans=(ans+g[i])%mod;
cout<<ans<<endl;
return 0;
}
II. P7091 數上的樹
首先將 \(n\) 分解質因數,用 DFS 求出 \(n\) 的所有因數,記為 \(d_1,d_2,\cdots,d_c\),跑一遍反素數那題的程式碼可知 \(c\leq 23327\)(
設 \(f_i\) 表示根節點為 \(d_i\) 時最小值。
顯然,區域性最優值可以保證整體最優值,且轉移無後效性,即求 \(f_i\) 時不會影響 \(f_j\ (d_j<d_i)\),故答案可以用樹形 DP 求出,將所有因數排序後可以轉化為序列上的 DP。
對於不能出現在樹上的 \(d_i\) 直接 skip 即可。
設 \(g_i\) 表示 \(d_i\) 所含有的質因子個數。For example,\(12=2\times 2\times 3\),所以 \(12\) 有 \(3\) 個質因子。在本題中,\(g_{i}\) 也表示以 \(d_i\) 為根的子樹的節點個數,不難發現其為定值。
假設當前轉移 \(f_i\) 決策點為 \(j,k\ (d_j\times d_k=d_i)\),那麼對於 \(d_j\) 和 \(d_k\) 子樹內兩兩組合出的 pairs 的貢獻可以直接由 \(f_j+f_k\) 推得,剩下來只有兩種情況:
- Case 1:\(d_j\) 和 \(d_k\) 子樹內各一個節點組合出的 pairs。因為它們的 LCA 是 \(d_i\),且共有 \(g_j\times g_k\) 對 pairs,故貢獻為 \(g_j\times g_k\times d_i\)。
- Case 2:\(d_i\) 和任意一個節點組合出的 pairs。顯然貢獻為 \(g_i\times d_i\)。
轉移方程:
\[f_i=\min_{d_j\times d_k=d_i}f_j+f_k+(g_j\times g_k+g_i)\times d_i \],其中 \(g_i=g_j+g_k+1\) 可以在 DP 時一併求出。
這樣子搞是 \(\mathcal O(c^3)\) 的,顯然無法接受。
- 剪枝 1:在列舉內層迴圈 \(j\) 時發現 \(k\) 有單調性,所以直接用 pointer 代替 \(k\) 即可。這樣時間複雜度降為了 \(\mathcal O(c^2)\)。
- 剪枝 2:當 \(j>k\) 時直接 break,減小常數。
綜上,我們有了一個 \({\mathcal O}(\sqrt n+m\log c+c^2)\) 的演算法(分解質因數 + 處理限制需要二分查詢 + DP),程式碼如下:
ll n,m,num[N],f[N];
ll cnt,pr[N],c[N],tot;
ll fc[N],il[N],d;
map <ll,int> isp;
void dfs(int pos,ll prod){
if(pos>cnt){
if(prod>1)fc[++d]=prod;
return;
} for(int i=0;i<=c[pos];i++)dfs(pos+1,prod),prod*=pr[pos];
}
int main(){
cin>>n>>m;
// factor
ll tmp=n;
for(ll i=2;i*i<=n;i++)
if(n%i==0){
pr[++cnt]=i,isp[i]=1;
while(n%i==0)c[cnt]++,tot++,n/=i;
}
if(n>1)pr[++cnt]=n,tot++,c[cnt]=1,isp[n]=1;
n=tmp;
// find factors
dfs(1,1);
sort(fc+1,fc+d+1);
// limit
for(int i=1;i<=m;i++){
ll val=read();
int pos=lower_bound(fc+1,fc+d+1,val)-fc;
if(pos<=d&&fc[pos]==val)il[pos]=1; // 表示 pos 不能出現
}
// dp
for(int i=1;i<=d;i++){
if(il[i])continue;
if(isp[fc[i]]){
num[i]=1,f[i]=fc[i];
continue;
} il[i]=1,f[i]=inf; // 如果無法由以前的 j,k 轉移得到那麼 i 也無法得到
int p=i-1;
for(int j=1;j<i;j++){
if(fc[i]%fc[j])continue;
while(fc[p]>fc[i]/fc[j])p--;
if(j>p)break;
if(!il[j]&&!il[p])f[i]=min(f[i],f[j]+f[p]+num[j]*num[p]*fc[i]+(num[i]=num[j]+num[p]+1)*fc[i]),il[i]=0;
}
} if(il[d])puts("-1");
else cout<<(ll)f[d]<<endl;
return 0;
}
III. CF1156F Card Bag
題意簡述:有 \(n\) 張卡牌,每張卡牌有數字 \(a_1,a_2,\cdots,a_n\)。現在隨機抽取卡牌,不放回,設本次抽到的卡牌為 \(x\),上次抽到的卡牌為 \(y\),若 \(x=y\) 則遊戲勝利;若 \(x<y\) 則輸掉遊戲;若 \(x>y\) 則遊戲繼續。求獲勝概率。
\(a_i\leq n\leq 5\times 10^3\)。
下文認為 \(a_i\) 與 \(n\) 同階。
不難發現我們只關心卡牌上的數字,所以開個桶維護每個數出現了幾次。又因為只能從小往大抽,即無後效性,所以考慮 DP。
設 \(f_{i,j}\) 為 共抽了 \(j\) 次,每個數最多抽到一次,最後一次抽到數字 \(i\) 的概率。
首先考慮如何轉移:我們設數字 \(i\) 共有 \(sz_i\) 個,那麼不難列出轉移方程
\[f_{i,j}=\sum_{k=0}^{i-1}f_{k,j-1}\times \frac{sz_i}{n-j+1} \],表示 在 \([1,i-1]\) 中抽了 \(j-1\) 個數 的概率乘上 抽到數字 \(i\) 的概率。這樣轉移的時間複雜度為 \(\mathcal{O}(n^3)\),無法接受。
如果設 \(s_{i,j}\) 為 在 \(i\) 中抽了 \(j\) 個數 的概率,則有
\[s_{i,j}=\sum_{k=1}^{i}f_{i,j} \],則轉移方程可變形為
\[f_{i,j}=\frac{s_{i-1,j-1}sz_i}{n-j+1} \]。預處理逆元做到時間複雜度 \(\mathcal{O}(n^2)\),可以接受。
這實際上就是具有實際意義的字首和優化。
最後使用滾動陣列可以將空間優化到 \(\mathcal{O}(n)\)。
需要注意初始值 \(f_{0,0}=1\)。
const int N=5e3+5;
ll n,ans,sz[N],f[2][N],s[2][N];
int main(){
init(),cin>>n,s[0][0]=s[1][0]=1;
for(int i=1,a;i<=n;i++)cin>>a,sz[a]++;
for(int i=1,p=1;i<=n;i++,p^=1){
for(int j=1;j<=i;j++){
f[p][j]=s[p^1][j-1]*sz[i]%mod*iv[n-j+1]%mod;
ans=(ans+s[p^1][j-1]*sz[i]*(sz[i]-1)%mod*iv[n-j+1]%mod*iv[n-j])%mod;
s[p][j]=(s[p^1][j]+f[p][j])%mod;
}
} cout<<ans<<endl;
return 0;
}
*IV. CF1542E2 Abnormal Permutation Pairs (hard version)
題解。
*V. AT693 文字列
很 nb 的題目,沒想出來。
注意我們不關注字元的相對順序,只關心有沒有相鄰的相同字元,因此考慮 DP:設 \(f_{i,j}\) 表示前 \(i\) 個字母構成的有 \(j\) 對相鄰字元的字串個數。轉移時列舉 \(j\),當前字母分幾段,以及一共插入到幾對相鄰字元中,然後組合數算算即可。答案即為 \(f_{n,0}\)。
時間複雜度 \(\alpha n^3\),其中 \(\alpha\) 是字符集大小,\(n\) 是字元總數。
可是我連第一步 DP 都沒想出來。
#pragma GCC optimize(3)
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define pb push_back
#define mem(x,v) memset(x,v,sizeof(x))
#define mcpy(x,y) memcpy(x,y,sizeof(y))
const ll mod=1e9+7;
const int N=260+5;
ll c,sum,a[N],f[N][N],C[N][N];
int main(){
for(int i=0;i<N;i++)
for(int j=0;j<=i;j++)
C[i][j]=(j==0||j==i?1:C[i-1][j-1]+C[i-1][j])%mod;
for(int i=0;i<26;i++)cin>>a[i];
f[0][max(0ll,a[0]-1)]=1,sum=a[0];
for(int i=1;i<26;i++){
if(!a[i])continue;
c++;
for(int j=0;j<=sum;j++)
for(int k=0;k<=a[i];k++)
for(int l=0;l<=k;l++)
if(l<=j){
int nw=j-l+(a[i]-k);
f[c][nw]=(f[c][nw]+f[c-1][j]*C[a[i]-1][k-1]%mod*C[j][l]%mod*C[sum+1-j][k-l])%mod;
}
sum+=a[i];
}
cout<<f[c][0]<<endl;
return 0;
}