再見了,所有的 Educational DP

Iictiw發表於2024-11-18

A - Frog 1

線性 DP。

狀態轉移方程為

\[f_i = \min(f_{i-1} + \lvert h_i - h_{i-1} \rvert, f_{i-2} + \lvert h_i - h_{i-2} \rvert) \]

,注意邊界。

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int n,a[N];
int f[N];
inline int ABS(int x){return max(x,-x);}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    f[2]=ABS(a[2]-a[1]);
    for(int i=3;i<=n;i++)
        f[i]=min(f[i-1]+ABS(a[i]-a[i-1]),f[i-2]+ABS(a[i]-a[i-2]));
    cout<<f[n]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

B - Frog 2

線性 DP。

狀態轉移方程為

\[f_i = \min_{i-k \leq j \lt i} (f_j + \lvert a_i - a_j \rvert) \]

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+5;
int n,m,a[N];
inline int ABS(int x){return max(x,-x);}
int f[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    memset(f,0x3f,sizeof(f));
    f[1]=0;
    for(int i=2;i<=n;i++)
        for(int j=max(1,i-m);j<i;j++)
            f[i]=min(f[i],f[j]+ABS(a[i]-a[j]));
    cout<<f[n]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

C - Vacation

線性 DP。

\(f_{i,0/1/2}\) 表示前 \(i\) 天,第 \(i\) 天選 A,B,C 的最大幸福值,轉移方程為

\[f_{i,0} = \max(f_{i-1,1},f_{i-1,2})+a_i \]

\[f_{i,1} = \max(f_{i-1,0},f_{i-1,2})+b_i \]

\[f_{i,2} = \max(f_{i-1,0},f_{i-1,1})+c_i \]

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
const int N=1e5+5;
int n,a[N],b[N],c[N];
int f[N][3];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i]>>b[i]>>c[i];
    for(int i=1;i<=n;i++){
        f[i][0]=max(f[i-1][1],f[i-1][2])+a[i];
        f[i][1]=max(f[i-1][0],f[i-1][2])+b[i];
        f[i][2]=max(f[i-1][0],f[i-1][1])+c[i];
    }
    cout<<max(max(f[n][0],f[n][1]),f[n][2])<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

D - Knapsack 1

揹包 DP。

\(f_{i,j}\) 為前 \(i\) 個物品,揹包容量為 \(j\) 的最大價值,狀態轉移方程為

\[f_{i,j} = \max(f_{i-1,j}, f_{i-1,j-w_i}+v_i) \]

使用滾動陣列或者一種廣為人知的動態更新可以做到 \(O(W)\) 空間。

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,m;
ll f[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1,v,w;i<=n;i++){
        cin>>w>>v;
        for(int j=m;j>=w;j--)f[j]=max(f[j],f[j-w]+v);
    }
    cout<<f[m]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

E - Knapsack 2

揹包 DP。

\(f_{i,j}\) 為前 \(i\) 個物品,價值為 \(j\) 的最小容量,狀態轉移方程為

\[f_{i,j} = \min(f_{i-1,j}, f_{i-1,j-v_i} + w_i) \]

同樣可以用滾動陣列或者一種廣為人知的動態更新可以做到 \(O(Nv)\) 空間。

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,m;
ll f[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    memset(f,0x3f,sizeof(f));
    f[0]=0;
    for(int i=1,w,v;i<=n;i++){
        cin>>w>>v;
        for(int j=100000;j>=v;j--)f[j]=min(f[j],f[j-v]+w);
    }
    for(int i=100000;~i;i--)
        if(f[i]<=m)return cout<<i<<endl,0;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

F - LCS

經典題。

\(f_{i,j}\)\(s[1..i]\)\(t[1..j]\) 的 LCS 長度,則轉移方程為

\[f_{i,j} = \begin{cases} \max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1}), s_i \neq t_j , \\ \max(f_{i-1,j},f_{i,j-1},f_{i-1,j-1} + 1), s_i = t_j . \end{cases} \]

記錄轉移以輸出方案。

程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using pii=pair<int,int>;
const int N=3005;
int n,m;
string s,t;
int f[N][N];
pii pa[N][N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>s>>t;n=s.size(),m=t.size();s=' '+s,t=' '+t;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++){
            if(f[i][j-1]>f[i-1][j])f[i][j]=f[i][j-1],pa[i][j]={i,j-1};
            else f[i][j]=f[i-1][j],pa[i][j]={i-1,j};
            if(f[i-1][j-1]+(s[i]==t[j])>f[i][j])f[i][j]=f[i-1][j-1]+(s[i]==t[j]),pa[i][j]={i-1,j-1};
        }
    string ans;
    pii p={n,m};
    while(p.first&&p.second){
        pii ne=pa[p.first][p.second];
        if(ne.first==p.first-1&&ne.second==p.second-1&&s[p.first]==t[p.second])ans+=s[p.first];
        p=ne;
    }
    reverse(ans.begin(),ans.end());
    cout<<ans<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

G - Longest Path

DAG 上 DP。

狀態轉移方程為

\[f_v = \max_{\{u \mid \exists (u,v) \in E \}}(f_u + 1) \]

程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#include<queue>
using namespace std;
const int N=1e5+5;
int n,m;
vector<int>gr[N];
int ind[N];
int f[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1,u,v;i<=m;i++){
        cin>>u>>v;
        gr[u].push_back(v),ind[v]++;
    }
    queue<int>q;
    for(int i=1;i<=n;i++)if(!ind[i])q.push(i);
    while(q.size()){
        int p=q.front();
        q.pop();
        for(auto to:gr[p]){
            f[to]=max(f[to],f[p]+1);
            if(!--ind[to])q.push(to);
        }
    }
    int ans=0;
    for(int i=1;i<=n;i++)ans=max(ans,f[i]);
    cout<<ans<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

H - Grid 1

狀態轉移方程為

\[f_{i,j} = \begin{cases} 0, a_{i,j} = \texttt{#} , \\ f_{i-1,j} + f_{i,j-1}, \texttt{otherwise} \end{cases} \]

程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1005;
int n,m;
char ch[N][N];
ll f[N][N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            cin>>ch[i][j];
    f[1][1]=1;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=m;j++)
            if(ch[i][j]=='.')(f[i][j]+=f[i-1][j]+f[i][j-1])%=mod;
    cout<<f[n][m]<<endl;
    return 0;
}

I - Coins

期望 DP。

\(f_{i,j}\) 為前 \(i\) 個硬幣,有 \(j\) 個正面的機率,轉移方程為

\[f_{i,j} \gets f_{i-1,j-1} \times p_i + f_{i-1,j} \times (1 - p_i) \]

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
const int N=3005;
int n;db a[N];
db f[2][N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    int cur=0;
    f[0][0]=1;
    for(int i=1;i<=n;i++){
        cur^=1;
        for(int j=0;j<=i;j++)f[cur][j]=0;
        f[cur][0]=f[cur^1][0]*(1-a[i]);
        for(int j=1;j<=i;j++)f[cur][j]+=f[cur^1][j]*(1-a[i])+f[cur^1][j-1]*a[i];
    }
    db ans=0;
    for(int i=n/2+1;i<=n;i++)ans+=f[cur][i];
    printf("%.9lf\n",ans);
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

J - Sushi

期望 DP。

\(f_{a,b,c}\) 為當前有 \(a\) 個盤子裝 \(1\) 個,\(b\) 個盤子裝 \(2\) 個,\(c\) 個盤子裝 \(3\) 個,期望還要操作幾次,可以得出

\[f_{a,b,c} = \frac{a}{n} f_{a-1,b,c} + \frac{b}{n} f_{a+1,b-1,c} + \frac{c}{n} f_{a,b-1,c+1} + \frac{n-a-b-c}{n} f_{a,b,c} + 1 \]

整理得

\[f_{a,b,c} = \frac{a f_{a-1,b,c} + b f_{a+1,b-1,c} + c f_{a,b+1,c-1} + n}{a+b+c} \]

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
const int N=305;
int n,a,b,c;
db f[N][N][N];
db dfs(int a,int b,int c){
    if(!a&&!b&&!c)return 0;
    if(a<0||b<0||c<0)return 0;
    if(f[a][b][c]!=-1)return f[a][b][c];
    db&ret=f[a][b][c];ret=0;
    ret=((db)a*dfs(a-1,b,c)+(db)b*dfs(a+1,b-1,c)+(db)c*dfs(a,b+1,c-1)+n)/(db)(a+b+c);
    return ret;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1,x;i<=n;i++){
        cin>>x;
        if(x==1)a++;
        else if(x==2)b++;
        else if(x==3)c++;
    }
    for(int i=0;i<=n;i++)
        for(int j=0;j<=n;j++)
            for(int k=0;k<=n;k++)
                f[i][j][k]=-1;
    printf("%.9lf\n",dfs(a,b,c));
    return 0;
}
//coder:Iictiw
//date:24/11/07
//When I wrote a piece of code, her and I knew what it was doing

K - Stones

博弈論。

\(f_i = 0/1\) 為當前狀態先手必敗 / 必勝,則有

\[f_i = \bigvee_{1 \leq j \leq n} \neg f_{i-a_j} \]

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
const int N=1e5+5;
int n,k,a[N];
int f[N];
bool dfs(int k){
    if(f[k]!=-1)return f[k];
    f[k]=0;
    for(int i=1;i<=n;i++)
        if(k>=a[i])f[k]|=!dfs(k-a[i]);
    return f[k];
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>k;
    for(int i=1;i<=n;i++)cin>>a[i];
    memset(f,-1,sizeof(f));
    if(dfs(k))cout<<"First"<<endl;
    else cout<<"Second"<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing

L - Deque

博弈論。

\(f_{l,r}\) 為當前序列為 \(a_l \ldots a_r\) 時的結果,轉移方程為

\[f_{l,r} = \max(- f_{l+1,r} + a_l, - f_{l,r-1} + a_r) \]

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=3005;
int n,a[N];
ll f[N][N];
inline ll dfs(int l,int r){
    if(l==r)return a[l];
    if(l>r)return 0;
    if(f[l][r]!=-1)return f[l][r];
    return f[l][r]=max(a[l]-dfs(l+1,r),a[r]-dfs(l,r-1));
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    memset(f,-1,sizeof(f));
    cout<<dfs(1,n)<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing

M - Candies

字首和最佳化 DP。

\(f_{i,j}\) 為前 \(i\) 個孩子,分了 \(j\) 個糖果的方案數,有轉移

\[\sum_{j-a_i \leq k \leq j} f_{i-1,k} \to f_{i,j} \]

易用字首和最佳化至 \(O(NK)\) 時間複雜度。

程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=105,M=1e5+5;
int n,m,a[N];
int f[2][M],g[M];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++)cin>>a[i];
    f[0][0]=1;
    int cur=0;
    for(int i=1;i<=n;i++){
        g[0]=f[cur][0];
        for(int j=1;j<=m;j++)g[j]=(g[j-1]+f[cur][j])%mod;
        cur^=1;
        for(int j=0;j<=m;j++)f[cur][j]=0;
        for(int j=0;j<=m;j++)f[cur][j]=((g[j]-(j-a[i]<=0?0:g[j-a[i]-1]))%mod+mod)%mod;
    }
    cout<<f[cur][m]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing

N - Slimes

區間 DP。

設 $ f_{l,r} $ 為區間 $ [l,r] $ 的最小代價,則轉移方程為

\[f_{l,r} = \min_{l \leq i \lt r}(f_{l,i} + f_{i+1,r} + \sum_{l \leq j \leq r} a_j) \]

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
using ll=long long;
const int N=405;
const ll inf=1e16+5;
int n,a[N];ll s[N];
ll f[N][N];
ll dfs(int l,int r){
    if(l>=r)return 0;
    if(f[l][r]!=-1)return f[l][r];
    ll&ret=f[l][r];ret=inf;
    for(int i=l;i<r;i++)
        ret=min(ret,dfs(l,i)+dfs(i+1,r)+s[r]-s[l-1]);
    return ret;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i],s[i]=s[i-1]+a[i];
    memset(f,-1,sizeof(f));
    cout<<dfs(1,n)<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing

O - Matching

狀壓 DP。

\(f_{S,i}\) 為匹配前 \(i\) 個男性,女性匹配情況為 \(S\) 的方案數,可得轉移方程

\[f_{S,i} \to f_{S \cup \{j\},i+1} ( S \cap \{j\} = \varnothing \wedge a_{i,j} = 1 ) \]

程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=21;
int n,a[N][N];
ll f[1<<N][N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>a[i][j];
    for(int i=0;i<n;i++)if(a[0][i])f[1<<i][0]=1;
    for(int i=0;i<n-1;i++)
        for(int S=0;S<(1<<n);S++){
            if(f[S][i]==0)continue;
            for(int j=0;j<n;j++){
                if((S>>j)&1||!a[i+1][j])continue;
                (f[S|(1<<j)][i+1]+=f[S][i])%=mod;
            }
        }
    cout<<f[(1<<n)-1][n-1]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/08
//When I wrote a piece of code, her and I knew what it was doing

P - Independent Set

樹上 DP。

\(f_{u,0/1}\) 為以第 \(u\) 個點為根的子樹,點 \(u\) 顏色為白 / 黑的方案數,轉移方程為

\[f_{u,0} = \prod_{\{v \mid v \in \operatorname{son}(u) \}} f_{v,0} + f_{v,1} \]

\[f_{u,1} = \prod_{\{v \mid v \in \operatorname{son}(u) \}} f_{v,0} \]

程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1e5+5;
int n;
vector<int>gr[N];
ll f[N][2];
void dfs(int p,int fa){
    f[p][0]=f[p][1]=1;
    for(auto to:gr[p]){
        if(to==fa)continue;
        dfs(to,p);
        (f[p][0]*=f[to][0]+f[to][1])%=mod;
        (f[p][1]*=f[to][0])%=mod;
    }
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        gr[u].push_back(v),gr[v].push_back(u);
    }
    dfs(1,0);
    cout<<(f[1][0]+f[1][1])%mod<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing

Q - Flowers

資料結構最佳化 DP。

\(f_{i}\) 為前 \(i\) 朵花,且選擇第 \(i\) 朵花的美麗值之和的最大值,有轉移

\[f_{i} = \max_{ \{j \mid 1 \leq j \lt i \wedge h_j < h_i \}} f_j + a_i \]

注意到這相當於一個二維偏序,因此可以用 BIT 最佳化至線性對數時間複雜度。

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,a[N],h[N];
ll f[N];
ll t[N];
inline void update(int i,ll x){for(i++;i<=n+1;i+=i&-i)t[i]=max(t[i],x);}
inline ll query(int i){ll ret=0;for(i++;i;i-=i&-i)ret=max(ret,t[i]);return ret;}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>h[i];
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int i=1;i<=n;i++){
        f[i]=query(h[i]-1)+a[i];
        update(h[i],f[i]);
    }
    cout<<query(n)<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing

R - Walk

矩陣乘法加速 DP。

\(f_{i,j}\) 為走了 \(i\) 步,當前在點 \(j\) 的方案數,有轉移

\[f_{i,j} = \sum_{\{ k \mid a_{k,j} = 1 \}} f_{i-1,k} \]

可以將轉移寫成矩陣乘法的形式,做到 \(O(n^3 \log k)\)

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=55;
int n;ll m;
struct matrix{
    ll a[N][N];
    int n,m;
    matrix(){
        n=m=0;
        memset(a,0,sizeof(a));
    }
    friend matrix operator*(matrix a,matrix b){
        matrix c;c.n=a.n,c.m=b.m;
        for(int k=1;k<=a.m;k++)
            for(int i=1;i<=a.n;i++)
                for(int j=1;j<=b.m;j++)
                    (c.a[i][j]+=a.a[i][k]*b.a[k][j])%=mod;
        return c;
    }
};
inline matrix qpow(matrix a,ll p){
    matrix ret;ret.n=a.n,ret.m=a.m;
    for(int i=1;i<=ret.n;i++)ret.a[i][i]=1;
    while(p){
        if(p&1)ret=ret*a;
        a=a*a,p>>=1;
    }
    return ret;
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    matrix I,C;C.n=n,C.m=n;
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++)
            cin>>C.a[i][j];
    I.n=1,I.m=n;
    for(int i=1;i<=n;i++)I.a[1][i]=1;
    C=qpow(C,m);
    I=I*C;
    ll ans=0;
    for(int i=1;i<=n;i++)(ans+=I.a[1][i])%=mod;
    cout<<ans<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing

S - Digit Sum

數位 DP。

\(f_{i,j}\) 為還有 \(i\) 位未填,數位之和模 \(D\) 等於 \(j\) 的方案數,有轉移

\[f_{i,j} = \sum f_{i-1,(j + k) \bmod D} \]

程式碼
#include<iostream>
#include<cstdio>
#include<cstring>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=1e4+5;
int n;string s;
ll f[N][105];
int num[N];
ll dfs(int p,int s,bool limit){
    if(!p)return s==0;
    if(!limit&&f[p][s]!=-1)return f[p][s];
    ll ret=0;int up=limit?num[p]:9;
    for(int i=0;i<=up;i++)(ret+=dfs(p-1,(s+i)%n,limit&&i==up))%=mod;
    if(!limit)f[p][s]=ret;
    return ret;
}
ll solve(string s){
    int tot=0;
    for(int i=(int)s.size()-1;i;i--)num[++tot]=s[i]-'0';
    memset(f,-1,sizeof(f));
    return dfs(tot,0,1);
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>s>>n;s=' '+s;
    cout<<(solve(s)-1+mod)%mod<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/09
//When I wrote a piece of code, her and I knew what it was doing

T - Permutation

字首和最佳化 DP。

開始上強度了。

\(f_{i,j}\) 為填前 \(i\) 個數,第 \(i\) 個數為 \(j\) ,且前 \(i\) 個數為 \(1\)\(i\) 的排列的方案數,每次加入新的數 \(j\) 時,可以將原來排列中所有大於等於 \(j\) 的數加一以保證仍是排列。不難發現,這樣依然滿足題目要求的性質。

由此,有轉移

\[ f_{i,j} = \begin{cases} \sum_{j \leq k \lt i} f_{i-1,k}, s_{i-1} = \texttt{>} \\ \sum_{1 \leq k \lt j} f_{i-1,k}, s_{i-1} = \texttt{<} \end{cases} \]

不難使用字首和最佳化轉移。

程式碼
#include<iostream>
#include<cstdio>
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=3005;
int n;string s;
ll f[N][N],g[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>s;s=' '+s;
    f[1][1]=1;
    for(int i=2;i<=n;i++){
        for(int j=1;j<i;j++)g[j]=(g[j-1]+f[i-1][j])%mod;
        for(int j=1;j<=i;j++)
            if(s[i-1]=='>')f[i][j]=(g[i-1]-g[j-1]+mod)%mod;
            else f[i][j]=g[j-1];
    }
    ll ans=0;
    for(int j=1;j<=n;j++)(ans+=f[n][j])%=mod;
    cout<<ans<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/10
//When I wrote a piece of code, her and I knew what it was doing

U - Grouping

狀壓 DP。

\(f_S\)\(S\) 中的兔子可得的最高得分,則有轉移

\[f_S = \max_{T \subseteq S}(f_{S-T} + g_T) \]

其中, \(g_S\)\(S\) 中的兔子分為一組的得分。

程式碼
#include<iostream>
#include<cstdio>
using namespace std;
using ll=long long;
const int N=17;
int n,a[N][N];
ll f[1<<N],g[1<<N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=0;i<n;i++)
        for(int j=0;j<n;j++)
            cin>>a[i][j];
    for(int S=0;S<(1<<n);S++)
        for(int i=0;i<n;i++){
            if(!((S>>i)&1))continue;
            for(int j=i+1;j<n;j++){
                if(!((S>>j)&1))continue;
                g[S]+=a[i][j];
            }
        }
    for(int S=0;S<(1<<n);S++)
        for(int T=S;T;T=S&(T-1))
            f[S]=max(f[S],f[S-T]+g[T]);
    cout<<f[(1<<n)-1]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/11
//When I wrote a piece of code, her and I knew what it was doing

V - Subtree

換根 DP。

設點 \(1\) 為根,\(f_{u}\) 為以 \(u\) 為根的子樹的方案數,則有轉移

\[f_{u} = \prod_{\{v \mid v \in \operatorname{son}(u)\}} f_{v} + 1 \]

考慮換根,設 \(g_{u}\) 為以 \(u\) 為根的子樹之外的方案數,\(fa\) 為點 \(u\) 的父親,則有轉移

\[g_{u} = g_{fa} (\prod_{\{v \mid v \in \operatorname{son}(fa) \wedge v \neq u\}} f_v + 1) + 1 \]

最終 \(f_u g_u\) 即為點 \(u\) 的答案。

考慮如何快速計算 \(\prod_{\{v \mid v \in \operatorname{son}(fa) \wedge v \neq u\}}\) ,一個直接的想法是從 \(fa\) 的子樹中扣去 \(u\) 子樹的答案,但由於模數非質且可能有 \(0\),逆元不一定存在。

考慮維護每個點的所有兒子的 \(f_u+1\) 的字首積和字尾積,計算時用前一個兄弟的字首積和後一個兄弟的字尾積計算答案。

程式碼
#include<iostream>
#include<cstdio>
#include<vector>
#define int long long
using namespace std;
using ll=long long;
const int N=1e5+5;
int n,mod;
vector<int>gr[N];
ll f[N],g[N],h[N];
void dfs(int p,int fa){
    f[p]=1;
    for(auto to:gr[p]){
        if(to==fa)continue;
        dfs(to,p),(f[p]*=f[to]+1)%=mod;
    }
    ll res=1;
    for(int i=0;i<(int)gr[p].size();i++){
        int to=gr[p][i];if(to==fa)continue;
        g[to]=res,(res*=f[to]+1)%=mod;
    }
    res=1;
    for(int i=gr[p].size()-1;~i;i--){
        int to=gr[p][i];if(to==fa)continue;
        (g[to]*=res)%=mod,(res*=f[to]+1)%=mod;
    }
}
void redfs(int p,int fa){
    if(p!=1)h[p]=(h[fa]*g[p]+1)%mod;
    for(auto to:gr[p])if(to!=fa)redfs(to,p);
}
signed main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>mod;
    for(int i=1,u,v;i<n;i++){
        cin>>u>>v;
        gr[u].push_back(v);gr[v].push_back(u);
    }
    dfs(1,0);
    h[1]=1,redfs(1,0);
    for(int i=1;i<=n;i++)cout<<h[i]*f[i]%mod<<'\n';
    return 0;
}
//coder:Iictiw
//date:24/11/11
//When I wrote a piece of code, her and I knew what it was doing

W - Intervals

資料結構最佳化 DP。

\(f_{i,j}\) 為考慮前 \(i\) 個位置,最近的 \(1\)\(j\) 的最大得分。

對於這類區間帶權的題,一種套路化的處理方法是在右端點處更新答案。考慮轉移,若在第 \(i\) 位放一個 \(1\),則有

\[f_{i,i} = \max_{1 \leq j \lt i}(f_{i-1,j} + \sum_{\{k\mid r_k = i \} } a_k) \]

若在第 \(i\) 位放一個 \(0\) ,則有

\[f_{i,j} = f_{i-1,j} + \sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k \]

直接做是 \(O(n^2)\) 的。不難發現,對於 \(i\) 相同的 \(f_{i,j}\)\(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 是相等的, \(f_i\) 相較於 \(f_{i-1}\) 只有 \(f_{i,i}\) 一個位置在去掉 \(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 後不同,於是可以讓 \(f_i\) 繼承 \(f_{i-1}\)\(1\)\(i-1\) 項,然後統一處理 \(\sum_{\{k\mid l_k \leq j \wedge r_k = i \} } a_k\) 對答案的影響,注意到第 \(k\) 個區間影響的 \(f_{i,j}\) 是滿足 $ j \in [l_k, r_k]$ 的一段 \(f_{i,j}\) ,於是可以用線段樹維護每個區間對答案的影響。

程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,m;
struct node{int l,r,x;}a[N];
#define mid ((l+r)>>1)
#define ls (p<<1)
#define rs (p<<1|1)
ll mx[N<<2],tag[N<<2];
inline void push_up(int p){mx[p]=max(mx[ls],mx[rs]);}
inline void make_tag(int p,ll x){mx[p]+=x,tag[p]+=x;}
inline void push_down(int p){
    if(tag[p]){
        make_tag(ls,tag[p]),make_tag(rs,tag[p]);
        tag[p]=0;
    }
}
void update(int p,int l,int r,int L,int R,ll x){
    if(l>=L&&r<=R)return make_tag(p,x);
    if(l>R||r<L)return;
    push_down(p);
    update(ls,l,mid,L,R,x),update(rs,mid+1,r,L,R,x);
    push_up(p);
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>m;
    for(int i=1;i<=m;i++)cin>>a[i].l>>a[i].r>>a[i].x;
    sort(a+1,a+1+m,[&](node x,node y){
        return x.r<y.r;
    });
    for(int i=1,j=1;i<=n;i++){
        update(1,1,n,i,i,max(mx[1],0ll));
        while(j<=m&&a[j].r<=i)update(1,1,n,a[j].l,a[j].r,a[j].x),j++;
    }
    cout<<max(0ll,mx[1])<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing

X - Tower

貪心維護 DP 轉移順序。

Lemma: 必然存在一種最優策略,使得若第 \(i\) 塊在第 \(j\) 塊下方,則有 \(s_i + w_i \geq s_j + w_j\)

證明:考慮鄰項交換,對於相鄰的兩個塊 \(i\)\(j\)\(j\)\(i\) 上時,\(i\) 的剩餘載量為 \(s_i - w_j\)\(i\)\(j\) 上時,\(j\) 的剩餘載量為 \(s_j - w_i\)。若 \(j\)\(i\) 上不劣於 \(i\)\(j\) 上,則必然有 \(s_i - w_j \geq s_j -w_i\) 。移項可得結論,進而容易推廣至任意 \(i,j\)

因此,可以將所有塊以 \(s_i + w_i\) 為關鍵字降序排序,然後考慮 DP。

\(f_{i,j}\) 為放了前 \(i\) 塊,還能承載重量為 \(j\) 的最大價值,則若不放第 \(i\) 塊,有轉移

\[f_{i,j} \gets f_{i-1,j} \]

若放第 \(i\) 塊在其他塊上,則有轉移

\[f_{i,\min(s_i,j - w_i)} \gets f_{i-1,j} + v_i \]

若第 \(i\) 塊作為最底部的塊,則有轉移

\[f_{i,s_i} \gets v_i \]

程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
using ll=long long;
const int N=1e4+5;
int n,m;
struct node{int w,s,v;}a[N];
ll f[2][N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i].w>>a[i].s>>a[i].v,m=max(m,a[i].s);
    sort(a+1,a+1+n,[&](node x,node y){
        return x.w+x.s>y.w+y.s;
    });
    int cur=0;
    f[0][a[1].s]=a[1].v;
    for(int i=2;i<=n;i++){
        cur^=1;
        for(int j=0;j<=m;j++)f[cur][j]=f[cur^1][j];
        f[cur][a[i].s]=max(f[cur][a[i].s],(ll)a[i].v);
        for(int j=0;j<=m;j++)
            if(a[i].w<=j)
                f[cur][min(a[i].s,j-a[i].w)]=max(f[cur][min(a[i].s,j-a[i].w)],f[cur^1][j]+a[i].v);
    }
    ll ans=0;
    for(int i=0;i<=m;i++)ans=max(ans,f[cur][i]);
    cout<<ans<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/14
//When I wrote a piece of code, her and I knew what it was doing

Y - Grid 2

容斥 DP。

直接計算方案數較為困難,考慮容斥,用總方案數減去經過牆的方案數。

一個直接的想法是經過 \(0\) 個牆的方案數 \(-\) 經過 \(1\) 個牆的方案數 \(+\) 經過 \(2\) 個牆的方案數 \(-\) 經過 \(3\) 個牆的方案數……但這種方式難以在低時間複雜度內計算。

考慮將經過牆的方案數不重不漏地表示出來。設 \(f_{i}\) 為在到達第 \(i\) 個牆前不經過其他任何牆的方案數,則可以同樣用容斥的方法計算 \(f_i\),即用到達第 \(i\) 個牆的方案數減去從其他牆到第 \(i\) 個牆的方案數,因此,有轉移

\[f_i = {{x_i+y_i-2}\choose{x_i-1}} - \sum_{\{j \mid x_j \leq x_i \wedge y_j \leq y_i\} } f_j {{x_i-x_j+y_i-y_j}\choose{x_i-y_i}} \]

特別地,我們不妨設第 \(n+1\) 個牆位於 \((h,w)\) ,則 \(f_{n+1}\) 即為答案。

程式碼
#include<iostream>
#include<cstdio>
#include<algorithm>
#define x first
#define y second
#define mod 1000000007
using namespace std;
using ll=long long;
const int N=2e5+5;
int n,m,k;
pair<int,int>a[N];
ll fct[N],ifct[N];
inline ll qpow(ll a,int p){
    ll ret=1;
    while(p){
        if(p&1)(ret*=a)%=mod;
        (a*=a)%=mod,p>>=1;
    }
    return ret;
}
inline ll C(int n,int m){
    if(n<0||m<0||n<m)return 0;
    return fct[n]*ifct[m]%mod*ifct[n-m]%mod;
}
ll f[N];
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    for(int i=fct[0]=1;i<N;i++)fct[i]=fct[i-1]*i%mod;
    ifct[N-1]=qpow(fct[N-1],mod-2);
    for(int i=N-2;~i;i--)ifct[i]=ifct[i+1]*(i+1)%mod;
    cin>>n>>m>>k;
    for(int i=1;i<=k;i++)cin>>a[i].x>>a[i].y;
    a[++k]={n,m};
    sort(a+1,a+1+k);
    for(int i=1;i<=k;i++){
        f[i]=C(a[i].x+a[i].y-2,a[i].x-1);
        for(int j=1;j<i;j++)
            if(a[j].y<=a[i].y)
                f[i]=(f[i]-f[j]*C(a[i].x-a[j].x+a[i].y-a[j].y,a[i].x-a[j].x)%mod+mod)%mod;
    }
    cout<<f[k]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/15
//When I wrote a piece of code, her and I knew what it was doing

Z - Frog 3

斜率最佳化 DP。

請自行腦補 BGM

\(f_i\) 為到點 \(i\) 的最小代價,\(O(n^2)\) 的轉移是容易的,即

\[f_i = \min_{1 \leq j \lt i}(f_j + (h_i - h_j)^2 + C) \]

不妨設此時有兩個決策點 \(j,k (j \lt k)\),我們稱對於點 \(i\) 來說決策點 \(j\) 優於 \(k\),當且僅當由 \(j\) 點轉移至 \(i\) 點的代價優於由 \(k\) 點轉移至 \(i\) 點的代價。若點 \(j\) 優於點 \(k\),則有

\[f_j + (h_i - h_j)^2 + C \lt f_k + (h_i - h_k)^2 + C \]

化簡得

\[f_j + {h_j}^2 - 2 h_i h_j \lt f_k + {h_k}^2 - 2 h_i h_k \]

注意到 \(f_j + {h_j}^2,f_k + {h_k}^2\) 分別只與 \(j,k\) 有關,設 \(g(i) = f_i + {h_i}^2\),則有

\[g(j) - 2h_ih_j \lt g(k) - 2h_ih_k \]

\[g(j) - g(k) \lt 2h_i(h_j - h_k) \]

由於題目保證 h 單調遞增,因此 \(h_j - h_k \lt 0\),於是最終得到

\[\dfrac{g(j) - g(k)}{h_j - h_k} \gt 2h_i \]

如果我們將 \(h_i\) 視為點 \(i\) 的橫座標, \(g(i)\) 視為點 \(i\) 的縱座標,這個式子就如同求點 \(j\) 與點 \(k\) 的連線的斜率。因此,這種最佳化方法稱為斜率最佳化。

類似與上式可得,若點 \(j\) 劣於點 \(k\),則有

\[\dfrac{g(j) - g(k)}{h_j - h_k} \lt 2h_i \]

若點 \(j\) 與點 \(k\) 不相上下,則有

\[\dfrac{g(j) - g(k)}{h_j - h_k} = 2h_i \]

我們不妨再設有 \(j_1,j_2,j_3 (j_1 \lt j_2 \lt j_3 \lt i)\) 三個決策點,思考若

\[\dfrac{g(j_1) - g(j_2)}{h_{j_1} - h_{j_2}} \gt \dfrac{g(j_2) - g(j_3)}{h_{j_2} - h_{j_3}} \]

意味著什麼。

分類討論,設 $slope(i,j) = \dfrac{g(i) - g(j)}{h_{i} - h_{j}} $ 以簡化式子。

  • \(2h_i \lt slope(j_2,j_3) \lt slope(j_1,j_2)\) 時:\(j_1\) 優於 \(j_2\)

  • \(slope(j_2,j_3) \leq 2h_i \leq slope(j_1,j_2)\) 時:\(j_1\) 不劣於 \(j_2\)\(j_3\) 不劣於 \(j_2\)

  • \(slope(j_2,j_3) \lt slope(j_1,j_2) \lt 2h_i\) 時:\(j_3\) 優於 \(j_2\)

不難發現,當 \(slope(j_2,j_3) \lt slope(j_1,j_2)\) 時, \(j_2\) 永遠不可能成為最優決策點。

在座標軸上,這表現為直線 \(j_1j_2\) 的斜率大於直線 \(j_2j_3\) 的斜率,此時 \(j_2\) 不可能成為最優決策點。

由此可得,所有可能的最優決策點滿足斜率單調遞增。

刪去所有不可能成為最優決策點的點,不難發現,剩下的點將會構成一個下凸殼:

因此,這種最佳化方法又稱為凸殼最佳化。

回到本題,考慮如何維護該下凸殼。不難發現本題中,決策點的橫座標(即 \(h_i\)) 單調遞增,因此每次只會在原凸殼的末尾加入一個點,可以用單調棧進行維護,以保證斜率單調遞增。

查詢時,即尋找斜率大於 \(2h_i\) 的最小的位於凸殼上的點,由於凸殼上斜率單調,可以用二分在凸殼上查詢出第一個斜率大於 \(2h_i\) 的點,做到 \(O(n\log n)\)

但在本題中, \(h_i\) 單調遞增,因此斜率(即 \(2h_i\))單調遞增,於是最優決策點也單調遞增,滿足決策單調性。所以,我們可以用單調佇列維護最優決策點,每次從隊首彈出斜率不大於 \(2h_i\) 的點,從而找到最優決策點,時間複雜度為 \(O(n)\)

結束了?並沒有。

從頭開始,回到原轉移方程 $ f_i = \min(f_j + {h_i}^2 - 2h_ih_j + {h_j}^2 + C) $ 。將僅與 \(i\) 有關的項和僅與 \(j\) 有關的項分離並去掉 \(min\),改寫為

\[f_j + {h_j}^2 = 2h_ih_j + f_i - {h_i}^2 - C \]

\(f_j + {h_j}^2 = y, 2h_i = k, h_j = x, f_i - {h_i}^2 - C = b\),則轉移方程相當於一個一次函式表示式 \(y = kx + b\) 。此時,\((x,y)\) 相當於平面上一個點, \(k\) 相當於直線斜率,\(b\) 表示過點 \((x,y)\) 的斜率為 \(k\) 的直線的截距。注意到 \(k\) 和所有 \((x,y)\) 都是已知的,我們的任務就轉化為選擇一個點 \(j(j \lt i)\),使得過點 \((x_j,y_j)\) (即 \((h_j,f_j + {h_j}^2)\)) 的斜率為 \(k_i\)(即 \(2h_i\))直線截距最小。

不妨假設有一條斜率為 \(k_i\) 的直線自底向上平移,其碰到的第一個點 \((x_j,y_j)\) 即為最優決策點(因為此時截距最小)。

可以看出,所有可能的最優決策點必然位於下凸殼上,每次要找的點也就是凸殼上第一個斜率大於 \(k_i\) 的點。

維護下凸殼和尋找最優決策點,這與上一種做法殊途同歸。

程式碼(二分棧)
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
using ll=long long;
const int N=2e5+5;
int n;ll C,a[N];
ll f[N];
int st[N],top;
inline db g(int i){return f[i]+a[i]*a[i];}
inline db slope(int i,int j){return (g(i)-g(j))/(a[i]-a[j]);}
inline int calc(db k){
    int l=1,r=top-1,ret=top;
    while(l<=r){
        int mid=(l+r)>>1;
        if(slope(st[mid],st[mid+1])>=k)r=mid-1,ret=mid;
        else l=mid+1;
    }
    return st[ret];
}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>C;
    for(int i=1;i<=n;i++)cin>>a[i];
    st[++top]=1;
    for(int i=2;i<=n;i++){
        int j=calc(2*a[i]);
        f[i]=f[j]+(a[i]-a[j])*(a[i]-a[j])+C;
        while(top>1&&slope(st[top-1],st[top])>=slope(st[top],i))top--;
        st[++top]=i;
    }
    cout<<f[n]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/18
//When I wrote a piece of code, her and I knew what it was doing
程式碼(單調佇列)
#include<iostream>
#include<cstdio>
using namespace std;
using db=double;
using ll=long long;
const int N=2e5+5;
int n;ll C,a[N];
ll f[N];
int L=0,R=-1,Q[N];
inline db g(int i){return f[i]+a[i]*a[i];}
inline db slope(int i,int j){return (g(i)-g(j))/(a[i]-a[j]);}
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0);
    cin>>n>>C;
    for(int i=1;i<=n;i++)cin>>a[i];
    Q[++R]=1;
    for(int i=2;i<=n;i++){
        while(L<R&&slope(Q[L],Q[L+1])<=2*a[i])L++;
        f[i]=f[Q[L]]+(a[i]-a[Q[L]])*(a[i]-a[Q[L]])+C;
        while(L<R&&slope(Q[R-1],Q[R])>=slope(Q[R],i))R--;
        Q[++R]=i;
    }
    cout<<f[n]<<endl;
    return 0;
}
//coder:Iictiw
//date:24/11/18
//When I wrote a piece of code, her and I knew what it was doing

後記

結束了?結束了。

結束了?沒結束。

結束了?結束了。

Fumo?fumo。

相關文章