再见了,所有的 Educational DP

A - Frog 1

线性 DP。

状态转移方程为

fi=min(fi1+|hihi1|,fi2+|hihi2|)

,注意边界。

代码
#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。

状态转移方程为

fi=minikj<i(fj+|aiaj|)

代码
#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。

fi,0/1/2 表示前 i 天,第 i 天选 A,B,C 的最大幸福值,转移方程为

fi,0=max(fi1,1,fi1,2)+ai

fi,1=max(fi1,0,fi1,2)+bi

fi,2=max(fi1,0,fi1,1)+ci

代码
#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。

fi,j 为前 i 个物品,背包容量为 j 的最大价值,状态转移方程为

fi,j=max(fi1,j,fi1,jwi+vi)

使用滚动数组或者一种广为人知的动态更新可以做到 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。

fi,j 为前 i 个物品,价值为 j 的最小容量,状态转移方程为

fi,j=min(fi1,j,fi1,jvi+wi)

同样可以用滚动数组或者一种广为人知的动态更新可以做到 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

经典题。

fi,js[1..i]t[1..j] 的 LCS 长度,则转移方程为

fi,j={max(fi1,j,fi,j1,fi1,j1),sitj,max(fi1,j,fi,j1,fi1,j1+1),si=tj.

记录转移以输出方案。

代码
#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。

状态转移方程为

fv=max{u(u,v)E}(fu+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

状态转移方程为

fi,j={0,ai,j=#,fi1,j+fi,j1,otherwise

代码
#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。

fi,j 为前 i 个硬币,有 j 个正面的概率,转移方程为

fi,jfi1,j1×pi+fi1,j×(1pi)

代码
#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。

fa,b,c 为当前有 a 个盘子装 1 个,b 个盘子装 2 个,c 个盘子装 3 个,期望还要操作几次,可以得出

fa,b,c=anfa1,b,c+bnfa+1,b1,c+cnfa,b1,c+1+nabcnfa,b,c+1

整理得

fa,b,c=afa1,b,c+bfa+1,b1,c+cfa,b+1,c1+na+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

博弈论。

fi=0/1 为当前状态先手必败 / 必胜,则有

fi=1jn¬fiaj

代码
#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

博弈论。

fl,r 为当前序列为 alar 时的结果,转移方程为

fl,r=max(fl+1,r+al,fl,r1+ar)

代码
#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。

fi,j 为前 i 个孩子,分了 j 个糖果的方案数,有转移

jaikjfi1,kfi,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。

fl,r 为区间 [l,r] 的最小代价,则转移方程为

fl,r=minli<r(fl,i+fi+1,r+ljraj)

代码
#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。

fS,i 为匹配前 i 个男性,女性匹配情况为 S 的方案数,可得转移方程

fS,ifS{j},i+1(S{j}=ai,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。

fu,0/1 为以第 u 个点为根的子树,点 u 颜色为白 / 黑的方案数,转移方程为

fu,0={vvson(u)}fv,0+fv,1

fu,1={vvson(u)}fv,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。

fi 为前 i 朵花,且选择第 i 朵花的美丽值之和的最大值,有转移

fi=max{j1j<ihj<hi}fj+ai

注意到这相当于一个二维偏序,因此可以用 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。

fi,j 为走了 i 步,当前在点 j 的方案数,有转移

fi,j={kak,j=1}fi1,k

可以将转移写成矩阵乘法的形式,做到 O(n3logk)

代码
#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。

fi,j 为还有 i 位未填,数位之和模 D 等于 j 的方案数,有转移

fi,j=fi1,(j+k)modD

代码
#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。

开始上强度了。

fi,j 为填前 i 个数,第 i 个数为 j ,且前 i 个数为 1i 的排列的方案数,每次加入新的数 j 时,可以将原来排列中所有大于等于 j 的数加一以保证仍是排列。不难发现,这样依然满足题目要求的性质。

由此,有转移

fi,j={jk<ifi1,k,si1=>1k<jfi1,k,si1=<

不难使用前缀和优化转移。

代码
#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。

fSS 中的兔子可得的最高得分,则有转移

fS=maxTS(fST+gT)

其中, gSS 中的兔子分为一组的得分。

代码
#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 为根,fu 为以 u 为根的子树的方案数,则有转移

fu={vvson(u)}fv+1

考虑换根,设 gu 为以 u 为根的子树之外的方案数,fa 为点 u 的父亲,则有转移

gu=gfa({vvson(fa)vu}fv+1)+1

最终 fugu 即为点 u 的答案。

考虑如何快速计算 {vvson(fa)vu} ,一个直接的想法是从 fa 的子树中扣去 u 子树的答案,但由于模数非质且可能有 0,逆元不一定存在。

考虑维护每个点的所有儿子的 fu+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。

fi,j 为考虑前 i 个位置,最近的 1j 的最大得分。

对于这类区间带权的题,一种套路化的处理方法是在右端点处更新答案。考虑转移,若在第 i 位放一个 1,则有

fi,i=max1j<i(fi1,j+{krk=i}ak)

若在第 i 位放一个 0 ,则有

fi,j=fi1,j+{klkjrk=i}ak

直接做是 O(n2) 的。不难发现,对于 i 相同的 fi,j{klkjrk=i}ak 是相等的, fi 相较于 fi1 只有 fi,i 一个位置在去掉 {klkjrk=i}ak 后不同,于是可以让 fi 继承 fi11i1 项,然后统一处理 {klkjrk=i}ak 对答案的影响,注意到第 k 个区间影响的 fi,j 是满足 j[lk,rk] 的一段 fi,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 块下方,则有 si+wisj+wj

证明:考虑邻项交换,对于相邻的两个块 ijji 上时,i 的剩余载量为 siwjij 上时,j 的剩余载量为 sjwi。若 ji 上不劣于 ij 上,则必然有 siwjsjwi 。移项可得结论,进而容易推广至任意 i,j

因此,可以将所有块以 si+wi 为关键字降序排序,然后考虑 DP。

fi,j 为放了前 i 块,还能承载重量为 j 的最大价值,则若不放第 i 块,有转移

fi,jfi1,j

若放第 i 块在其他块上,则有转移

fi,min(si,jwi)fi1,j+vi

若第 i 块作为最底部的块,则有转移

fi,sivi

代码
#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 个墙的方案数……但这种方式难以在低时间复杂度内计算。

考虑将经过墙的方案数不重不漏地表示出来。设 fi 为在到达第 i 个墙前不经过其他任何墙的方案数,则可以同样用容斥的方法计算 fi,即用到达第 i 个墙的方案数减去从其他墙到第 i 个墙的方案数,因此,有转移

fi=(xi+yi2xi1){jxjxiyjyi}fj(xixj+yiyjxiyi)

特别地,我们不妨设第 n+1 个墙位于 (h,w) ,则 fn+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

fi 为到点 i 的最小代价,O(n2) 的转移是容易的,即

fi=min1j<i(fj+(hihj)2+C)

不妨设此时有两个决策点 j,k(j<k),我们称对于点 i 来说决策点 j 优于 k,当且仅当由 j 点转移至 i 点的代价优于由 k 点转移至 i 点的代价。若点 j 优于点 k,则有

fj+(hihj)2+C<fk+(hihk)2+C

化简得

fj+hj22hihj<fk+hk22hihk

注意到 fj+hj2,fk+hk2 分别只与 j,k 有关,设 g(i)=fi+hi2,则有

g(j)2hihj<g(k)2hihk

g(j)g(k)<2hi(hjhk)

由于题目保证 h 单调递增,因此 hjhk<0,于是最终得到

g(j)g(k)hjhk>2hi

如果我们将 hi 视为点 i 的横坐标, g(i) 视为点 i 的纵坐标,这个式子就如同求点 j 与点 k 的连线的斜率。因此,这种优化方法称为斜率优化。

类似与上式可得,若点 j 劣于点 k,则有

g(j)g(k)hjhk<2hi

若点 j 与点 k 不相上下,则有

g(j)g(k)hjhk=2hi

我们不妨再设有 j1,j2,j3(j1<j2<j3<i) 三个决策点,思考若

g(j1)g(j2)hj1hj2>g(j2)g(j3)hj2hj3

意味着什么。

分类讨论,设 slope(i,j)=g(i)g(j)hihj 以简化式子。

  • 2hi<slope(j2,j3)<slope(j1,j2) 时:j1 优于 j2

  • slope(j2,j3)2hislope(j1,j2) 时:j1 不劣于 j2j3 不劣于 j2

  • slope(j2,j3)<slope(j1,j2)<2hi 时:j3 优于 j2

不难发现,当 slope(j2,j3)<slope(j1,j2) 时, j2 永远不可能成为最优决策点。

在坐标轴上,这表现为直线 j1j2 的斜率大于直线 j2j3 的斜率,此时 j2 不可能成为最优决策点。

由此可得,所有可能的最优决策点满足斜率单调递增。

删去所有不可能成为最优决策点的点,不难发现,剩下的点将会构成一个下凸壳:

因此,这种优化方法又称为凸壳优化。

回到本题,考虑如何维护该下凸壳。不难发现本题中,决策点的横坐标(即 hi) 单调递增,因此每次只会在原凸壳的末尾加入一个点,可以用单调栈进行维护,以保证斜率单调递增。

查询时,即寻找斜率大于 2hi 的最小的位于凸壳上的点,由于凸壳上斜率单调,可以用二分在凸壳上查找出第一个斜率大于 2hi 的点,做到 O(nlogn)

但在本题中, hi 单调递增,因此斜率(即 2hi)单调递增,于是最优决策点也单调递增,满足决策单调性。所以,我们可以用单调队列维护最优决策点,每次从队首弹出斜率不大于 2hi 的点,从而找到最优决策点,时间复杂度为 O(n)

结束了?并没有。

从头开始,回到原转移方程 fi=min(fj+hi22hihj+hj2+C) 。将仅与 i 有关的项和仅与 j 有关的项分离并去掉 min,改写为

fj+hj2=2hihj+fihi2C

fj+hj2=y,2hi=k,hj=x,fihi2C=b,则转移方程相当于一个一次函数表达式 y=kx+b 。此时,(x,y) 相当于平面上一个点, k 相当于直线斜率,b 表示过点 (x,y) 的斜率为 k 的直线的截距。注意到 k 和所有 (x,y) 都是已知的,我们的任务就转化为选择一个点 j(j<i),使得过点 (xj,yj) (即 (hj,fj+hj2)) 的斜率为 ki(即 2hi)直线截距最小。

不妨假设有一条斜率为 ki 的直线自底向上平移,其碰到的第一个点 (xj,yj) 即为最优决策点(因为此时截距最小)。

可以看出,所有可能的最优决策点必然位于下凸壳上,每次要找的点也就是凸壳上第一个斜率大于 ki 的点。

维护下凸壳和寻找最优决策点,这与上一种做法殊途同归。

代码(二分栈)
#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。

posted @   Iictiw  阅读(6)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
点击右上角即可分享
微信分享提示