樸素 DP
[ABC301F] Anti-DDoS
題意
link
定義形如 DDoS
的序列為類 DDoS 序列,其中 DD
表示兩個相同的任意大寫字母,o
表示任意小寫字母,S
表示任意大寫字母。
給定一個由大小寫字母和 ?
組成的序列 \(S\),問有多少種將 ?
替換為大小寫字母的方案可以使 \(S\) 不含有任何一個類 DDoS 子序列,答案對 \(998244353\) 取模。
\(4 \le \left|S\right| \le 3 \times 10^5\)。
解法
這是上一道例題的變式。
這一道題因為是對不含類 DDoS 子序列的方案計數,所以為了方便,我們設 \(f_{i,j}\) 是前 \(i\) 位中沒有類 DDoS 子序列中的前 \(j+2\) 位的方案數。顯然答案就是 \(f_{n,2}\)。
首先我們考慮如何計算 \(f_{i,0}\),即使前 \(i\) 位中不含兩個相同大寫字母的方案數。考慮假設前 \(i\) 位有 \(m\) 個 ?
,有 \(k\) 種大寫字母。注意到如果這 \(k\) 種大寫字母的總個數不為 \(k\),那麼此時方案數一定為 \(0\)。否則我們可以在 \(m\) 個 ?
選取 \(0\sim k\) 個選擇大寫字母,其餘選擇小寫字母,這樣我們可以列出式子:
然後考慮如何計算 \(f_{i,1}\) 和 \(f_{i,2}\)。對於不存在 DDo
的方案數,我們發現如果一個位置是大寫字母,那麼這裡我們就只需要保證之前不存在 DDo
就行了;而如果一個位置是小寫字母,我們這裡則要保證之前不存在 DD
;如果是 ?
的話,等於說這裡任意小寫或大寫字母都可以填,於是有轉移式:
對 \(f_{i,2}\) 的轉移類似。
這樣我們就在 \(O(n|\Sigma|)\) 的時間複雜度內解決了此題。
程式碼
#include<bits/stdc++.h>
using namespace std;
string s;
#define int long long
int f[300005][5];
int frac[300005],ifrac[300005],_26[300005];
const int mod=998244353;
int ksm(int a,int b){
if(!b)return 1;
return (b&1?a:1)*ksm(a*a%mod,b/2)%mod;
}
int vis[27],lftc=26;
int A(int a,int b){
if(a<b)return 0;
return frac[a]*ifrac[a-b]%mod;
}
int C(int a,int b){
if(a<b)return 0;
return frac[a]*ifrac[a-b]%mod*ifrac[b]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>s;
int siz=s.size();
frac[0]=_26[0]=1;
int lim=max(26ll,siz);
for(int i=1;i<=lim;i++)frac[i]=frac[i-1]*i%mod,_26[i]=_26[i-1]*26%mod;
ifrac[lim]=ksm(frac[lim],mod-2);
for(int i=lim-1;i>=0;i--)ifrac[i]=ifrac[i+1]*(i+1)%mod;
int cntq=0;
f[0][0]=f[0][1]=f[0][2]=1;
for(int i=1;i<=siz;i++){//DD
if(s[i-1]=='?')cntq++;
else if(s[i-1]>='A'&&s[i-1]<='Z'){
if(vis[s[i-1]-'A'+1])break;
else vis[s[i-1]-'A'+1]=1,lftc--;
}
int &p=f[i][0];
for(int j=min(lftc,cntq);j>=0;j--){
p=(p+C(cntq,j)*A(lftc,j)%mod*_26[cntq-j]%mod)%mod;
// cout<<j<<" "<<cntq<<" "<<lftc<<" "<<C(cntq,j)<<" "<<A(lftc,j)<<" "<<frac[26]<<" "<<p<<"\n";
}
}
// cout<<"\n";
for(int i=1;i<=siz;i++){//DDo
if(s[i-1]=='?')f[i][1]=(26ll*f[i-1][0]%mod+26ll*f[i-1][1]%mod)%mod;
else if(s[i-1]>='a'&&s[i-1]<='z')f[i][1]=f[i-1][0];
else f[i][1]=f[i-1][1];
}
for(int i=1;i<=siz;i++){
if(s[i-1]=='?')f[i][2]=(26ll*f[i-1][1]%mod+26ll*f[i-1][2]%mod)%mod;
else if(s[i-1]>='a'&&s[i-1]<='z')f[i][2]=f[i-1][2];
else f[i][2]=f[i-1][1];
}
cout<<f[siz][2];
return 0;
}
P2224 [HNOI2001] 產品加工
題意
link
某加工廠有 A、B 兩臺機器,來加工的產品可以由其中任何一臺機器完成,或者兩臺機器共同完成。由於受到機器效能和產品特性的限制,不同的機器加工同一產品所需的時間會不同,若同時由兩臺機器共同進行加工,所完成任務又會不同。
某一天,加工廠接到 \(n\) 個產品加工的任務,每個任務的工作量不盡一樣。
你的任務就是:已知每個任務在 A 機器上加工所需的時間 \(t_1\),B 機器上加工所需的時間 \(t_2\) 及由兩臺機器共同加工所需的時間 \(t_3\),請你合理安排任務的排程順序,使完成所有 \(n\) 個任務的總時間最少。
\(1\leq n\leq 6\times 10^3\),\(0\leq t_1,t_2,t_3\leq 5\)。
解法
和上一道題有一些相似之處。
注意到題面沒有說非要按順序完成這些任務,直接按順序加入元素顯然可能會導致等待,這樣不同順序會有不同的 DP 結果,所以我們需要一種不導致等待或者可以按某個順序 DP 的 DP 方式。
考慮假設這個時候我們已經透過 DP 算出了一個前 \(i\) 個元素的排程順序。
這個時候我們對 A 或 B 設定一個單獨的任務,並不會產生額外的工作時間變化。
但是我們對 A,B 設定一個一起做的任務,我們發現此時 B 就需要被迫等待 A 做完剩下的才能和 A 一起做。這個時候我們把這個任務插到開頭,發現就沒有這個等待的時間了,這時因為沒有多餘時間,所以一定最優。
因為最優情況下一定沒有等待時間,所以原問題就變成了有一些任務,選一些給 A 做,選一些給 B 做,再選一些讓它們一起做,求兩個機器運作的時間的最大值的最小值,這樣每個元素都是獨立的,加入時不受前面或後面元素的影響,這樣就能 DP 了。
所以我們設 \(f_{i,j}\) 為前 \(i\) 個元素中,A 機器執行了 \(j\) 時間,B 機器執行的最小時間。轉移是簡單的,就討論這個任務是由 A 做還是由 B 做還是一起做。有:
這樣我們能在 \(O(nV)\) 時間複雜度內解決這個問題,其中 \(V=5n\)。
程式碼
#include<bits/stdc++.h>
using namespace std;
int n;
const int M=3e4;
int dp[2][M+5];
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
int now=1,ed=0,t1,t2,t3;
memset(dp[0],0x3f,sizeof dp[0]);
dp[0][0]=0;
cin>>n;
for(int i=1;i<=n;i++){
cin>>t1>>t2>>t3;
memset(dp[now],0x3f,sizeof dp[now]);
for(int j=0;j<=M;j++){
if(t1&&j>=t1)dp[now][j]=min(dp[now][j],dp[ed][j-t1]);
if(t2)dp[now][j]=min(dp[now][j],dp[ed][j]+t2);
if(t3&&j>=t3)dp[now][j]=min(dp[now][j],dp[ed][j-t3]+t3);
}
swap(now,ed);
}
int ans=1e9;
for(int i=0;i<=M;i++)ans=min(ans,max(i,dp[ed][i]));
cout<<ans;
return 0;
}
區間 DP
P2470 [SCOI2007] 壓縮
link
題意
給一個由小寫字母組成的字串,我們可以用一種簡單的方法來壓縮其中的重複資訊。壓縮後的字串除了小寫字母外還可以(但不必)包含大寫字母R與M,其中M標記重複串的開始,R重複從上一個M(如果當前位置左邊沒有M,則從串的開始算起)開始的解壓結果(稱為緩衝串)。
bcdcdcdcd
可以壓縮為 bMcdRR
,下面是解壓縮的過程:
已經解壓的部分 | 解壓結果 | 緩衝串 |
---|---|---|
b | b | b |
bM | b | . |
bMc | bc | c |
bMcd | bcd | cd |
bMcdR | bcdcd | cdcd |
bMcdRR | bcdcdcdcd | cdcdcdcd |
\(n\leq 50\)。
解法
和上道題一樣的壓縮字串類的題。
其實這題可以加一個輸出方案,這樣的話這題就是一個作者認為非常好的例題。
因為這題要處理 M
,所以我們可以設 \(f_{i,j}\) 為區間 \([i,j]\) 可以用 R
字元壓縮的最短長度。
這裡就有兩個轉移,第一個是 \(f_{i,j}\gets f_{i,i+\frac{j-i+1}{2}-1}+1\),壓縮一半。
第二個是合併兩個區間,我們有 \(f_{i,j}\gets f_{i,k}+(j-k+1)\),因為第二個區間不能有 R
。
然後我們考慮把所有區間的 \(f\) 用另外一個 DP 合併起來,設 \(g_{i}\) 為前 \(i\) 個元素的壓縮後最短長度,顯然有 \(g_{i}\gets g_j+f_{j+1,i}+1\),\(1\) 是給前面加的 M
加的。
這兩個方程式都很像能 DP 最佳化的樣子,如果胡出來這個最佳化的可以私信作者。
UPD:作者胡了一個 \(O(n^2)\) 的掃描一遍 + 單調棧的做法,但是這題 \(n\leq 50\) 隨便過。
最後答案就是 \(g_{1,n}-1\)。
程式碼
#include<bits/stdc++.h>
using namespace std;
char s[55],*ss=s+1;
int dp[55][55];
int f[55];
#define ull unsigned long long
ull hsh[55];
const ull base=179;
ull _b[55];
ull gethsh(int l,int r){
return hsh[r]-hsh[l-1]*_b[r-l+1];
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>ss;
int n=strlen(ss);
_b[0]=1;
for(int i=1;i<=n;i++)hsh[i]=hsh[i-1]*base+s[i]-'a'+1,_b[i]=_b[i-1]*base;
for(int i=1;i<=n;i++)f[i]=1e9;
memset(dp,0x3f,sizeof dp);
for(int i=1;i<=n;i++)dp[i][i]=1;
for(int i=2;i<=n;i++){
for(int j=1;j<=n-i+1;j++){
for(int k=j;k<j+i-1;k++){
dp[j][j+i-1]=min(dp[j][j+i-1],dp[j][k]+(j+i-1)-k);
}
if(i%2==0){
if(gethsh(j,j+i/2-1)==gethsh(j+i/2,j+i-1))dp[j][j+i-1]=min(dp[j][j+i-1],dp[j][j+i/2-1]+1);//...R
}
}
}
// cerr<<dp[1][4]<<" "<<gethsh(1,2)<<" "<<gethsh(3,4)<<"\n";
for(int i=1;i<=n;i++){
for(int j=0;j<i;j++){
f[i]=min(f[i],f[j]+dp[j+1][i]+1);
}
}
cout<<f[n]-1;
return 0;
}
P3592 [POI2015] MYJ
link
題意
有 \(n\) 家洗車店從左往右排成一排,每家店都有一個正整數價格 \(p_i\)。有 \(m\) 個人要來消費,第 \(i\) 個人會駛過第 \(a_i\) 個開始一直到第 \(b_i\) 個洗車店,且會選擇這些店中最便宜的一個進行一次消費。但是如果這個最便宜的價格大於 \(c_i\),那麼這個人就不洗車了。請給每家店指定一個價格,使得所有人花的錢的總和最大。
\(n\leq 50,m\leq 4000\)。
解法
和上一道題基本一樣,而且要輸出方案,所以是選做。
離散化 \(c_i\),設 \(f_{l,r,p}\) 為區間 \([l,r]\) 的定價都不小於 \(p\) 的時候,對於行駛區間完全包含於 \([l,r]\) 的人的花費最大值。
考慮隨意指定這個區間的一個位置,欽定其定價為 \(p\),因為其餘定價都不小於 \(p\),所以行駛跨過該位置的人如果要花費就可以在這裡花費。所以我們有:
其中 \(cnt_{pos,p}\) 表示行駛區間在 \([l,r]\) 內且跨越 \(pos\) 位置又能接受 \(p\) 價格的人的數量。
這個方程式顯然不完整,因為它只包含了這個區間有定價為 \(p\) 的點的情況。如果整個區間都是 \(>p\) 的定價,那麼我們完全可以從 \(f_{l,r,p+1}\) 轉移過來。這樣的轉移就完整了。
最後一個問題就是如何計算 \(cnt_{pos,p}\)。我們在列舉 \(l,r,pos\) 的時候我們可以列舉每一個人計算,不難發現每個人都是接受一個價格字首的,所以我們可以差分。這樣我們可以在 \(O(m)\) 解決這個問題。
輸出方案的話同樣記錄 \(f_{l,r,p}\) 是從哪個位置還是從 \(f_{l,r,p+1}\) 轉移過來,最後 DFS 一遍即可。
程式碼
#include<bits/stdc++.h>
using namespace std;
int n,m;
int cval[4005],ccnt;
int l[4005],r[4005],c[4005];
int f[55][55][4005],ans[55],o[55][55][4005];
int tmp[4005];
const int SIG=1e8;
void getans(int l,int r,int val){
if(l>r)return;
if(o[l][r][val]==0){
for(int i=l;i<=r;i++)ans[i]=cval[val];
return;
}
else if(o[l][r][val]==SIG)getans(l,r,val+1);
else{
ans[o[l][r][val]]=cval[val];
getans(l,o[l][r][val]-1,val);
getans(o[l][r][val]+1,r,val);
}
return;
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++)cin>>l[i]>>r[i]>>c[i],cval[++ccnt]=c[i];
sort(cval+1,cval+ccnt+1);
ccnt=unique(cval+1,cval+ccnt+1)-cval-1;
for(int i=1;i<=m;i++){
c[i]=lower_bound(cval+1,cval+ccnt+1,c[i])-cval;
}
for(int i=1;i<=n;i++){
for(int j=1;j<=n-i+1;j++){
for(int k=j;k<=j+i-1;k++){
memset(tmp,0,sizeof tmp);
for(int p=1;p<=m;p++)if(l[p]>=j&&l[p]<=k&&r[p]>=k&&r[p]<=j+i-1)tmp[c[p]]++;
for(int p=ccnt;p>=1;p--)tmp[p]+=tmp[p+1];
for(int p=1;p<=ccnt;p++){
if(f[j][j+i-1][p]<tmp[p]*cval[p]+f[j][k-1][p]+f[k+1][j+i-1][p]){
f[j][j+i-1][p]=tmp[p]*cval[p]+f[j][k-1][p]+f[k+1][j+i-1][p];
o[j][j+i-1][p]=k;
}
}
}
for(int p=ccnt;p>=1;p--){
if(f[j][j+i-1][p]<f[j][j+i-1][p+1]){
f[j][j+i-1][p]=f[j][j+i-1][p+1];
o[j][j+i-1][p]=SIG;
}
}
}
}
cout<<f[1][n][1]<<"\n";
getans(1,n,1);
for(int i=1;i<=n;i++)cout<<ans[i]<<" ";
return 0;
}
揹包 DP
P1941 [NOIP2014 提高組] 飛揚的小鳥
link
題意
Flappy Bird 是一款風靡一時的休閒手機遊戲。玩家需要不斷控制點選手機螢幕的頻率來調節小鳥的飛行高度,讓小鳥順利透過畫面右方的管道縫隙。如果小鳥一不小心撞到了水管或者掉在地上的話,便宣告失敗。
為了簡化問題,我們對遊戲規則進行了簡化和改編:
遊戲介面是一個長為 \(n\),高為 \(m\) 的二維平面,其中有 \(k\) 個管道(忽略管道的寬度)。
小鳥始終在遊戲介面內移動。小鳥從遊戲介面最左邊任意整數高度位置出發,到達遊戲介面最右邊時,遊戲完成。
小鳥每個單位時間沿橫座標方向右移的距離為 \(1\),豎直移動的距離由玩家控制。如果點選螢幕,小鳥就會上升一定高度 \(x\),每個單位時間可以點選多次,效果疊加;如果不點選螢幕,小鳥就會下降一定高度 \(y\)。小鳥位於橫座標方向不同位置時,上升的高度 \(x\) 和下降的高度 \(y\) 可能互不相同。
小鳥高度等於 \(0\) 或者小鳥碰到管道時,遊戲失敗。小鳥高度為 \(m\) 時,無法再上升。
現在,請你判斷是否可以完成遊戲。如果可以,輸出最少點選螢幕數;否則,輸出小鳥最多可以透過多少個管道縫隙。
\(n\leq 10000,m\leq 1000\)。
解法
直接從左到右掃一遍,如果遇到管道那就設定強制不可達。
特判下降和最高點的轉移,中間的轉移列舉同餘系然後掃一遍中間的數就行了。揹包的轉移是簡單的。
不是依賴性揹包。
程式碼
#include<bits/stdc++.h>
using namespace std;
int dp[2][1005];
int n,m,k;
int upo[10005],downo[10005];
int pos[10005],L[10005],R[10005];
int id[10005];
int main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=1;i<=n;i++)cin>>upo[i]>>downo[i];
for(int i=1;i<=k;i++)cin>>pos[i]>>L[i]>>R[i],id[pos[i]]=i;
int now=1,ed=0;
dp[ed][0]=1e9;
int cnt=0;
for(int i=1;i<=n;i++){
memset(dp[now],0x3f,sizeof dp[now]);
for(int j=1;j<m;j++)dp[now][m]=min(dp[now][m],dp[ed][j]+(m-j+(upo[i]-1))/upo[i]);
dp[now][m]=min(dp[now][m],dp[ed][m]+1);
for(int j=m-downo[i];j>=1;j--)dp[now][j]=min(dp[now][j],dp[ed][j+downo[i]]);
for(int j=1;j<=upo[i];j++){
int tmp=1e9;
for(int k=j;k<m;k+=upo[i]){
dp[now][k]=min(dp[now][k],tmp+1);
tmp=min(tmp+1,dp[ed][k]);
}
}
if(id[i]){
for(int j=1;j<=L[id[i]];j++)dp[now][j]=1e9;
for(int j=R[id[i]];j<=m;j++)dp[now][j]=1e9;
cnt++;
}
bool flag=0;
for(int j=1;j<=m;j++)if(dp[now][j]<1e8)flag=1;
if(!flag)return cout<<0<<"\n"<<cnt-1,0;
swap(now,ed);
}
int ans=1e9;
for(int i=1;i<=m;i++){
ans=min(dp[ed][i],ans);
}
cout<<1<<"\n"<<ans;
return 0;
}