動態規劃23題解析

最近兩週作了動態規劃的23道經典好題,涉及到區間、樹形、數位等三種動態規劃類型,如今將這23道題的題解寫在下面,方便你們借鑑以及我加深記憶。node

upd at:20190815 13:41.T14週年記念晚會ios

一、石子合併ide

經典的區間DP問題,枚舉合併的堆數做爲階段,設f[i][j]表示i->j這段區間內的最優方案,考慮在這段區間內枚舉斷點k,不可貴到f[i][j]=min(f[i][k]+f[k+1][j]+sum(i,j))(最大值同理)。破環爲鏈後直接進行DP便可。ui

#include<iostream> #include<cstring> #include<cstdio> 
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[1005],f[505][505],g[505][505],prefix[505],minn=21374404,maxn; int main() { //freopen("A.in","r",stdin); //freopen("A.out","w",stdout);
    memset(f,20,sizeof(f)); n=read(); for(int i=1;i<=n;i++) { a[i]=read(); a[i+n]=a[i]; } for(int i=1;i<=n*2;i++) { prefix[i]=prefix[i-1]+a[i]; f[i][i]=0; } for(int i=2;i<=n;i++) { for(int j=1;j<=2*n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k+1][end]+prefix[end]-prefix[j-1]); g[j][end]=max(g[j][end],g[j][k]+g[k+1][end]+prefix[end]-prefix[j-1]); } } } for(int i=1;i<=n;i++) { minn=min(minn,f[i][i+n-1]); maxn=max(maxn,g[i][i+n-1]); } printf("%d\n%d\n",minn,maxn); //fclose(stdin); //fclose(stdout);
    return 0; }
石子合併

二、能量項鍊spa

同「石子合併」,將第一題的求和換成題目指定的模擬規則便可。設計

#include<iostream> #include<cstring> #include<cstdio> 
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[1005],f[505][505],b[1005],maxn; int main() { //freopen("B.in","r",stdin); //freopen("B.out","w",stdout);
    n=read(); for(int i=1;i<=n;i++) { a[i]=read(); a[i+n]=a[i]; } for(int i=1;i<=n*2-1;i++) { b[i]=a[i+1]; } b[n*2]=a[1]; for(int i=2;i<=n;i++) { for(int j=1;j<=2*n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=max(f[j][end],f[j][k]+f[k+1][end]+a[j]*b[k]*b[end]); } } } for(int i=1;i<=n;i++) { maxn=max(maxn,f[i][i+n-1]); } printf("%d\n",maxn); //fclose(stdin); //fclose(stdout);
    return 0; }
能量項鍊

三、凸多邊形的劃分3d

設f[i][j]爲i號節點到j號節點組成的凸多邊形的最優剖分,咱們能夠在這段區間內找到一個非i、j的頂點k,剖分出一個以i,j,k爲頂點的三角形和兩個凸多邊形.code

而後問題就轉化成了如何求這兩個小凸多邊形的和,從小區間到大區間轉移的過程當中,小區間是已經被計算過的,直接調用便可。blog

狀態轉移方程以下:f[i][j]=min(f[i][j],f[i][k]+f[k][j]+a[i]*a[j]*a[k]);遞歸

注意,此題須要用高精度或int128

#include<iostream> #include<cstdio> #include<cstring>
#define int __uint128_t
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } void myitoa(int a,char* s) { int w=0; while(a>0) { s[++w]=a%10+'0'; a/=10; } s[0]=w; } int n,a[505],f[110][110]; char s[110]; signed main() { //freopen("C.in","r",stdin); //freopen("C.out","w",stdout);
    n=read(); for(int i=1;i<=n;i++)a[i]=read(); for(int i=2;i<=n-1;i++) { for(int j=1;j<=n-i;j++) { int end=i+j; f[j][end]=1e30; for(int k=j+1;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k][end]+a[j]*a[k]*a[end]); } } } myitoa(f[1][n],s); for(int i=s[0];i>=1;i--) { cout<<s[i]; } //fclose(stdin); //fclose(stdout);
    return 0; }
凸多邊形的劃分

四、括號匹配

經過題目很容易發現這道題的邊界值:當i和i+1能夠匹配的時候,它們合併的代價爲0,不然爲2。直接按照區間DP模板進行合併,兩部分的代價加起來取最小就是最優值,特別地,當兩個端點自己就能夠匹配的時候,還要和中間的再取一次min。

#include<iostream> #include<cstring> #include<cstdio>
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } char a[155]; int n,f[155][155]; int main() { memset(f,0x3f,sizeof(f)); cin>>(a+1); n=strlen(a+1); for(int i=1;i<=n;i++)f[i][i]=1; for(int i=1;i<=n;i++) { if(a[i]=='('&&a[i+1]==')')f[i][i+1]=0; else if(a[i]=='['&&a[i+1]==']')f[i][i+1]=0; else f[i][i+1]=2; } for(int i=2;i<=n;i++) { for(int j=1;j<=n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { f[j][end]=min(f[j][end],f[j][k]+f[k+1][end]); } if(a[j]=='('&&a[end]==')')f[j][end]=min(f[j][end],f[j+1][end-1]); else if(a[j]=='['&&a[end]==']')f[j][end]=min(f[j][end],f[j+1][end-1]); } } cout<<f[1][n]<<endl; return 0; }
括號匹配

五、分離與合體

須要記錄合併的地方,而後遞歸輸出便可。

#include<iostream> #include<cstring> #include<cstdio>
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,a[505],f[505][505],b[505][505]; void dfs(int x,int y,int dep,int k) { if(x>=y)return; if(dep==k) { printf("%d ",b[x][y]); return; } dfs(x,b[x][y],dep+1,k); dfs(b[x][y]+1,y,dep+1,k); } int main() { n=read(); for(int i=1;i<=n;i++)a[i]=read(); for(int i=2;i<=n;i++) { for(int j=1;j<=n-i+1;j++) { int end=i+j-1; for(int k=j;k<end;k++) { if(f[j][k]+f[k+1][end]+(a[j]+a[end])*a[k]>f[j][end]) { f[j][end]=f[j][k]+f[k+1][end]+(a[j]+a[end])*a[k]; b[j][end]=k; } } } } printf("%d\n",f[1][n]); for(int i=1;i<n;i++) { dfs(1,n,1,i); } }
分離與合體

六、矩陣取數遊戲

DP五分鐘,高精兩小時!!!!!!這題DP其實還蠻好想的,由於小區間是由大區間轉移而來,因此每個區間要麼是由左邊取一個數後獲得,要麼是由右邊取一

數獲得,不可貴到以下的轉移方程:

f[i][j]=max(f[i-1][j]+base[m-j+i-1]*a[i-1],f[i][j+1]+base[m-j+i-1]*a[j+1]);

最後因爲空區間沒法表示,最後還要再取一邊max。

#include<iostream> #include<cstring> #include<cstdio>
#define int long long
using namespace std; int read() { int x=0,f=1;char ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();} if(f)return x;return -x; } int n,m,a[110],f[110][110][110],base[110][110],ans[110],maxn,s1[110],s2[110],s3[110],s4[110],s5[110],s6[110],s7[110],s8[110]; void Mark(int c[]) { for(int i=1;i<=30;i++) { c[i+1]+=c[i]/10000; c[i]%=10000; } for(int i=30;i>=1;i--) { if(c[i]) { c[0]=i;break; } } } void Mul(int a[],int b,int c[]) { for(int i=1;i<=a[0];i++)c[i]=a[i]*b; Mark(c); } void Add(int a[],int b[],int c[]) { Mark(a);Mark(b); if(a[0]>b[0])c[0]=a[0]; else c[0]=b[0]; for(int i=1;i<=c[0];i++)c[i]=a[i]+b[i]; Mark(c); } bool compare(int a[],int b[]) { Mark(a);Mark(b); if(a[0]<b[0])return 0;if(a[0]>b[0])return 1; for(int i=a[0];i>=1;i--) { if(a[i]<b[i])return 0; if(b[i]<a[i])return 1; } return 0; } void pre() { base[0][0]=base[0][1]=1; for(int i=1;i<=m;i++)Mul(base[i-1],2,base[i]); } void write(int ans[]) { cout<<ans[ans[0]]; for(int i=ans[0]-1;i>=1;i--) { cout<<ans[i]/1000; cout<<ans[i]/100%10; cout<<ans[i]/10%10; cout<<ans[i]%10; } } signed main() { //freopen("F.in","r",stdin); //freopen("F.out","w",stdout);
    n=read();m=read(); pre(); while(n--) { memset(f,0,sizeof(f)); memset(s5,0,sizeof(s5)); memset(s6,0,sizeof(s6)); for(int i=1;i<=m;i++)a[i]=read(); for(int i=1;i<=m;i++) { for(int j=m;j>=i;j--) { memset(s1,0,sizeof(s1)); memset(s2,0,sizeof(s2)); memset(s7,0,sizeof(s7)); memset(s8,0,sizeof(s8)); Mul(base[m-j+i-1],a[i-1],s1); Add(s1,f[i-1][j],s7); Mul(base[m-j+i-1],a[j+1],s2); Add(s2,f[i][j+1],s8); if(compare(s7,s8))memcpy(f[i][j],s7,sizeof(f[i][j])); else memcpy(f[i][j],s8,sizeof(f[i][j])); } } for(int i=1;i<=m;i++) { memset(s3,0,sizeof(s3)); memset(s4,0,sizeof(s4)); Mul(base[m],a[i],s3); Add(f[i][i],s3,s4); if(compare(s4,s5))memcpy(s5,s4,sizeof(s5)); } Add(ans,s5,s6); memcpy(ans,s6,sizeof(ans)); } write(ans); //fclose(stdin); //fclose(stdout);
    return 0; } /* 2 10 96 56 54 46 86 12 23 88 80 43 16 95 18 29 30 53 88 83 64 67 */
矩陣取數遊戲

七、二叉蘋果樹

建樹後依次枚舉給左右子樹保留的樹枝,並不難的記憶化搜索。

#include<iostream> #include<cstring>
using namespace std; int mp[1005][1005],n,q,l[1005],r[1005],a[1005],f[1005][1005]; void Maketree(int node) { for(int i=1;i<=n;i++) { if(mp[node][i]==-1)continue; l[node]=i;a[i]=mp[node][i]; mp[node][i]=mp[i][node]=-1; Maketree(i); break; } for(int i=1;i<=n;i++) { if(mp[node][i]==-1)continue; r[node]=i;a[i]=mp[node][i]; mp[node][i]=mp[i][node]=-1; Maketree(i); break; } } int dfs(int u,int w) { if(w==0)return 0; if(l[u]==0&&r[u]==0)return a[u]; if(f[u][w])return f[u][w]; for(int i=0;i<=w-1;i++) { f[u][w]=max(f[u][w],dfs(l[u],i)+dfs(r[u],w-i-1)+a[u]); } return f[u][w]; } int main() { ios::sync_with_stdio(0);cin.tie(0);cout.tie(0); memset(mp,-1,sizeof(mp)); cin>>n>>q; for(int x,y,z,i=1;i<n;i++) { cin>>x>>y>>z; mp[x][y]=mp[y][x]=z; } Maketree(1); cout<<dfs(1,q+1)<<endl; return 0; }
二叉蘋果樹

 八、選課
首先把先修課設爲該門課程的父親,咱們就獲得了一棵以0號節點爲根的樹,設f[x][j]表示以x爲根的子樹,因爲每一個節點每種狀態只能有一個狀態轉移到父親節點。

咱們就能夠創建一個分組揹包模型,用記憶化搜索求解便可

#include<iostream>
#include<cstring>
#include<cstdio>
#include<vector>
using namespace std;
int read()
{
    int x=0,f=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=0;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=(x<<3)+(x<<1)+(ch^48);ch=getchar();}
    return x*f;
}
int n,m,s[505],f[505][505];
vector<int>son[505];
void dp(int x)
{
    for(int i=0;i<son[x].size();i++)
    {
        int v=son[x][i];
        dp(v);
        for(int t=m;t>=0;t--)
        {
            for(int j=t;j>=0;j--)
            {
                f[x][t]=max(f[x][t],f[x][t-j]+f[v][j]);
            }
        }
    }
    if(x!=0)
    {
        for(int t=m;t>0;t--)
        {
            f[x][t]=f[x][t-1]+s[x];
        }
    }
    return;
}
int main()
{
    n=read();m=read();
    for(int i=1;i<=n;i++)
    {
        int x;
        x=read();s[i]=read();
        son[x].push_back(i);
    }
    dp(0);
    cout<<f[0][m]<<endl;
    return 0;
}
選課

九、數字轉換

能夠先把全部數的約數和求出來,而後把符合條件的兩個數之間建邊,最後以1爲根求樹的直徑便可。

#include<iostream>
using namespace std;
int n,sum[50005],v[500005],head[500005],nxt[500005],cnt,ans,d[50005];
bool vis[50005];
void add(int a,int b)
{
    v[++cnt]=b;
    nxt[cnt]=head[a];
    head[a]=cnt;
}
void dp(int x)
{
    for(int i=head[x];i;i=nxt[i])
    {
        int y=v[i];
        if(vis[y])continue;
        vis[y]=1;
        dp(y);
        ans=max(ans,d[x]+d[y]+1);
        d[x]=max(d[x],d[y]+1);
    }
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        for(int j=2;i*j<=n;j++)
        {
            sum[i*j]+=i;
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(sum[i]<i)
        {
            add(sum[i],i);
        }
    }
    dp(1);
    cout<<ans<<endl;
    return 0;
}
數字轉換

十、戰略遊戲

題意大體爲要咱們求一個點集,使得每條邊上至少有一個頂點屬於該點集。

這個問題又叫作「樹的最大獨立集」問題,能夠用樹形DP求解。

因爲每個點都有取和不取兩種狀態,所以咱們能夠針對兩種狀況進行討論:

設f[x][0]爲x號節點不選,以x爲根的子樹符合條件的最小值,因爲該點不選,因此其全部兒子節點必須選,則該狀態的狀態轉移方程爲:f[x][0]=∑f[y][1](y∈son[x])

設f[x][1]表示x節點選,以x爲根的子樹符合條件的最小值,因爲該點必選,因此其全部兒子都可選可不選,取最小值便可,DP方程:f[x][1]=∑min(f[y][0],f[y][1])(y∈son[x])

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,s,k,t,v[50005],head[50005],nxt[50005],cnt,f[3505][2];
void add(int a,int b)
{
    v[++cnt]=b;
    nxt[cnt]=head[a];
    head[a]=cnt;
}
void dp(int x,int fa)
{
    f[x][1]=1;
    for(int i=head[x];i;i=nxt[i])
    {
        int y=v[i];
        if(y==fa)continue;
        dp(y,x);
        f[x][0]+=f[y][1];
        f[x][1]+=min(f[y][0],f[y][1]);
    }
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>s>>k;s++;
        for(int i=1;i<=k;i++)
        {
            cin>>t;t++;
            add(s,t);add(t,s);
        }
    }
    dp(1,0);
    cout<<min(f[1][0],f[1][1])<<endl;
    return 0;
}
戰略遊戲

十一、皇宮看守

這題和T10差很少,只不過是由邊變成了點,能夠設計三種狀態分別對應父親看守、兒子看守、和本身看守。

f[x][0]表示父親看守,因此這個節點不用放兵,他的子節點本身解決或者讓孫子節點解決,取小便可f[x][0]=∑min(f[y][1],f[y][2])(y∈son[x])

f[x][1]表示兒子看守,這個時候x不用看守,兒子節點期望不上父親看守,因此只能選擇孫子看守和本身看守,須要注意的是,若是全部節點都讓兒子看守(也就是x的孫子),

那麼還須要挑一個差值最小的補上來看父親。

f[x][2]表示本身看守,此時兒子三種選擇都可,取小便可。

#include<iostream>
using namespace std;
int n,k,dl,xps,r,a[1550],v[150005],head[150005],nxt[150005],cnt,f[1550][3],vis[1555];
void add(int a,int b)
{
    v[++cnt]=b;
    nxt[cnt]=head[a];
    head[a]=cnt;
}
void dp(int x)
{
    f[x][2]=a[x];
    int d=21374404;
    for(int i=head[x];i;i=nxt[i])
    {
        int y=v[i];
        dp(y);
        f[x][0]+=min(f[y][1],f[y][2]);
        f[x][1]+=min(f[y][1],f[y][2]);
        f[x][2]+=min(f[y][0],min(f[y][1],f[y][2]));
        d=min(d,f[y][2]-min(f[y][1],f[y][2]));
    }
    f[x][1]+=d;
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)
    {
        cin>>dl;
        cin>>a[dl]>>k;
        for(int j=1;j<=k;j++)
        {
            cin>>xps;
            add(dl,xps);
            //add(xps,dl);
            vis[xps]++;
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(!vis[i])
        {
            r=i;
            break;
        }
    }
    dp(r);
    //for(int i=1;i<=n;i++)cout<<i<<" "<<a[i]<<endl;
    cout<<min(f[r][1],f[r][2]);
    return 0;
}
皇宮看守

十二、加分二叉樹

二叉樹的基本概念不在這裏細說,這題本質上是個區間DP,枚舉斷點做爲子樹的根,最後遞歸遍歷輸出便可。

#include<iostream>
#define int long long
using namespace std;
int n,f[505][505],root[505][505];
void write(int l,int r)
{
    if(l>r)return;
    if(l==r&&r!=n){cout<<l<<" ";return;}
    else if(l==r){cout<<l;return;}
    if(root[l][r]!=n)cout<<root[l][r]<<" ";
    else cout<<root[l][r];
    write(l,root[l][r]-1);
    write(root[l][r]+1,r);
}
signed main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>f[i][i],f[i][i-1]=1,root[i][i]=i;
    for(int i=2;i<=n;i++)
    {
        for(int j=1;j<=n-i+1;j++)
        {
            int end=i+j-1;
            f[j][end]=f[j+1][end]+f[j][j];
            root[j][end]=j;
            for(int k=j+1;k<=end-1;k++)
            {
                if(f[j][end]<f[j][k-1]*f[k+1][end]+f[k][k])
                {
                    f[j][end]=f[j][k-1]*f[k+1][end]+f[k][k];
                    root[j][end]=k;
                }
            }
        }
    }
    cout<<f[1][n]<<endl;
    write(1,n);
    return 0;
}
加分二叉樹

1三、旅遊規則

這道題考察對樹的直徑的靈活運用。樹的直徑能夠看作是兩條鏈拼成的,因此在樹的直徑上的點必定能夠引出沒有公共邊的兩條鏈,總和加起來爲樹的直徑,咱們只需枚舉每個點,看他往父親節點引邊收益的最大值以及次短鏈的最大值,加起來和最長鏈拼在一塊兒,若是是直徑就是答案,特別地,要注意該點自己就在父親節點的最長鏈上,須要特殊處理

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
int n,v[400005],head[400005],nxt[400005],cnt,f[200005][3],ans,son[200005];
void add(int a,int b)
{
    v[++cnt]=b;
    nxt[cnt]=head[a];
    head[a]=cnt;
}
void dp(int x,int fa)
{
    for(int i=head[x];i!=-1;i=nxt[i])
    {
        int y=v[i];
        if(y==fa)continue;
        dp(y,x);
        int k=f[y][0]+1;
        if(k>f[x][0]){f[x][1]=f[x][0];f[x][0]=k;son[x]=y;}
        else if(k>f[x][1]){f[x][1]=k;}
        ans=max(ans,f[x][0]+f[x][1]);
    }
}
void dfs(int x,int fa)
{
    if(x!=0)
    {
        if(son[fa]!=x)f[x][2]=1+max(f[fa][2],f[fa][0]);
        else f[x][2]=1+max(f[fa][2],f[fa][1]);
    }
    for(int i=head[x];i!=-1;i=nxt[i])
    {
        int y=v[i];
        if(y==fa)continue;
        dfs(y,x);
    }
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    memset(head,-1,sizeof(head));memset(son,-1,sizeof(son));
    cin>>n;
    for(int x,y,i=1;i<n;i++)
    {
        cin>>x>>y;
        add(x,y);add(y,x);
    }
    dp(0,-1);
    dfs(0,-1);
    for(int i=0;i<n;i++)if(max(f[i][2],f[i][1])+f[i][0]==ans)cout<<i<<endl;
    //cout<<ans<<endl;
    //for(int i=0;i<n;i++)cout<<f[i][0]<<" "<<f[i][1]<<" "<<f[i][2]<<endl;
    return 0;
}
旅遊規則

1四、週年記念晚會

同「戰略遊戲」,只不過由必須有端點變成不能同時有兩個端點。

#include<iostream>
using namespace std;
int n,a[6005],f[6005][2],v[15005],head[15005],nxt[15005],cnt,root;
bool vis[6005];
void add(int a,int b)
{
    v[++cnt]=b;
    nxt[cnt]=head[a];
    head[a]=cnt;
}
void dp(int x)
{
    f[x][1]=a[x];
    for(int i=head[x];i;i=nxt[i])
    {
        int y=v[i];
        dp(y);
        f[x][0]+=max(f[y][0],f[y][1]);
        f[x][1]+=f[y][0];
    }
}
int main()
{
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n;
    for(int i=1;i<=n;i++)cin>>a[i];
    for(int x,y,i=1;i<n;i++)
    {
        cin>>x>>y;
        add(y,x);
        vis[x]=1;
    }
    for(int i=1;i<=n;i++)
    {
        if(!vis[i])
        {
            root=i;
            break;
        }
    }
    dp(root);
    cout<<max(f[root][0],f[root][1])<<endl;
    return 0;
}
週年記念晚會
相關文章
相關標籤/搜索