DP那些事

一.基础DP:

一般的DP的步骤:

1.定义状态:找通解
(1)关注题目需要的东西

(2)状态确定,输出就确定

(3)将状态的准确描述记录下来

2.寻找状态转移方程:描述一个子问题如何用更小的子问题得到
3.确定边界条件:最小的子问题或不满足状态转移方程的状态

典型例题

例一 P1077 摆花

\(dp_{i,j}\) 表示前 i 种花一共摆了 j 盆时的种类数

然后让每一位加上前面可以达到的位的答案

答案即为 \(dp_{n,m}\)

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    dp[0][0]=1;
    for(register int i=1;i<=n;i++)
        for(register int j=0;j<=m;j++)
            for(register int k=0;k<=a[i];k++)
                if(j>=k)dp[i][j]=(dp[i][j]+dp[i-1][j-k])%MOD;//需要保证摆的花不超过j盆且不超过限制a[i]
    printf("%d",dp[n][m]);
    return 0;
}

例二 P1233 木棍加工

一眼可以看出是求下降子序列的个数。

下降子序列的个数等于最长上升子序列的长度。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e3+5;

struct Node
{
    int l,w;
}node[MAXN];

int n,ans;
int dp[MAXN];

inline bool cmp(Node a,Node b)
{
    if(a.l!=b.l)
        return a.l>b.l;
    return a.w>b.w;
}

int main()
{
    scanf("%d",&n);
    for(register int i=1;i<=n;i++)
    {
        scanf("%d%d",&node[i].l,&node[i].w);
    }
    sort(node+1,node+n+1,cmp);
    for(register int i=1;i<=n;i++)
    {
        dp[i]=1;
        for(register int j=1;j<=i-1;j++)
        {
            if(node[i].w>node[j].w)
            {
                dp[i]=max(dp[i],dp[j]+1);
            }
        }
    }
    int maxi=0;
    for(register int i=1;i<=n;i++)
        maxi=max(maxi,dp[i]);
        printf("%d",maxi);
    return 0;
}

例三 P1103 书本整理

不整齐度和剩下的书有关,所以状态和剩下的书有关。

\(dp_{i,j}\) 表示前 \(i\) 本书中留下了 $ j$ 本且第 \(i\) 本一定选的最小不整齐度。

寻找每一个可以与当前遍历到的 \(i\) 匹配的 \(p\) 即可。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;

struct Node
{
    int h,w;
}node[105];

inline bool cmp(Node x,Node y)
{
    return x.h<y.h;
}

int n,k;
int dp[105][105];

int main()
{
    scanf("%d%d",&n,&k);
    for(register int i=1;i<=n;i++)
    {
        scanf("%d%d",&node[i].h,&node[i].w);
    } 
    sort(node+1,node+1+n,cmp);
    for(register int i=1;i<=n;i++)
        for(register int j=1;j<=n-1;j++)
            dp[i][j]=1e9;
    for(register int i=1;i<=n;i++)
        dp[i][0]=dp[i][1]=0;
    for(register int i=1;i<=n;i++)
        for(register int j=1;j<=n-k;j++)
            for(register int p=j-1;p<=i-1;p++)
                dp[i][j]=min(dp[i][j],(dp[p][j-1]+abs(node[p].w-node[i].w)));
    int mini=1e9;
    for(register int i=n-k;i<=n;i++)
        mini=min(mini,dp[i][n-k]);
    printf("%d",mini);
    return 0;
}

例四 CF577B Modulo Sum

直接搞DP是 \(O(nm)\) 的,肯定会炸。

考虑一个小优化:当 \(n≥m\) 时必定有解。

我们假设 \(dp_{i,j}\) 表示考虑在前 i 个数中选数,是否可能使得它们的和除以 $m $ 的余数为 \(j\) ,初始状态 \(dp_{i,ai} =1\),枚举每个数和余数进行转移即可。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
int n,m;
bool dp[MAXN][MAXN];
int a[MAXN];
int main()
{
    scanf("%d%d",&n,&m);
    if(n>m)
    {
        puts("YES");
        return 0;
    }
    for(register int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i]%=m;
        dp[i][a[i]]=1;
        if(!a[i])
        {
            puts("YES");
            return 0;
        }
    }
    for(register int i=1;i<=n;i++)
    {
        for(register int j=0;j<m;j++)
        {
            dp[i][j]|=dp[i-1][j];
            dp[i][(j+a[i])%m]|=dp[i-1][j];
        }
        if(dp[i][0])
        {
            puts("YES");
            return 0;
        }
    }
    puts("NO");
    return 0;
}

例五 P2196 挖地雷

有很多方法,比如暴搜。

可能第一印象是拓扑+DP

手玩可以发现,一定是从编号小的地窖跑到编号较大的地窖。

所以二维循环即可。

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;

int n,a,y,x;
int d[30],w[30],id[30],nxt[30];
bool linker[30][30];

inline int count(int u)
{
    if(d[u])
        return d[u];
    int v=0,k=-1;
    for(register int i=1;i<=n;i++)
        if(linker[u][i])
        {
            if(d[i]==0)
                d[i]=count(i);
            if(d[i]>v)
                v=d[i],k=i;
        }
    nxt[u]=k;
    return v+w[u];
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
    {
        cin>>w[i];
        id[i]=0;
        nxt[i]=-1;
    }
    for(register int i=1;i<=n;i++)
        for(register int j=i+1;j<=n;j++)
        {
            cin>>a;
            if(a)linker[i][j]=true,id[j]++;
        }
    for(register int i=1;i<=n;i++)
        if(id[i]==0)
        {
            d[i]=count(i);
            if(d[i]>y)y=d[i],x=i;
        }
    while(x!=-1)
    {
        printf("%d ",x);
        x=nxt[x];
    }
    printf("\n%d",y);
    return 0;
}

二.区间DP

1.定义

一般是求一个区间内的最大值,最小值,方案数。

2.判别:

(1)从不同位置开始递推得到的结果可能不一样。

(2)合并类或拆分类。

3.循环:

区间DP一般有三个循环

(1)第一个循环一般是枚举阶段(子问题)

(2)第二个循环枚举所有的状态(情形)

(3)第三个循环枚举决策点(从哪里转移)

典型例题

例一 P1775 石子合并(弱化版)

最经典的区间DP题,没什么好说

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=305;

int n;
int a[MAXN];
int dp[MAXN][MAXN];
int sum[MAXN];

int main()
{
    scanf("%d",&n);
    memset(dp,0x7f,sizeof dp);
    for(register int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        sum[i]=sum[i-1]+a[i];
        dp[i][i]=0;
    }
    for(register int len=2;len<=n;len++)
        for(register int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            for(register int k=i;k<j;k++)
                dp[i][j]=min(dp[i][k]+dp[k+1][j]+sum[j]-sum[i-1],dp[i][j]);
        }
    printf("%d",dp[1][n]);
    return 0;
}

例二 P3146 248 G

还是区间DP板子。

只要两边相等,就可以将两边合并。

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;

int n;
int a[255];
int dp[255][255];
int ans;

int main()
{
    scanf("%d",&n);
    memset(dp,-0x7f,sizeof dp);
    for(register int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        dp[i][i]=a[i];
    }
    for(register int len=2;len<=n;len++)
        for(register int i=1;i<=n-len+1;i++)
        {
            int j=i+len-1;
            for(register int k=i;k<j;k++)
            {
                if(dp[i][k]==dp[k+1][j])dp[i][j]=max(dp[i][j],dp[i][k]+1);
            }
        }
    for(register int i=1;i<=n;i++)
        for(register int j=i+1;j<=n;j++)
            ans=max(ans,dp[i][j]);
    printf("%d",ans);
    return 0;
}

例三 P2890 Cheapest Palindrome G

注意转移就行......

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
int n,m;
string a;
int dp[MAXN][MAXN];
map<char,int>ins,del;
signed main()
{
    scanf("%d%d",&n,&m);
    cin>>a;
    a=' '+a;
    memset(dp,0x7f,sizeof dp);
    for(register int i=1;i<=n;i++)
    {
        char op;
        int in,de;
        cin>>op>>in>>de;
        ins[op]=in;
        del[op]=de;
    }
    for(register int i=1;i<=m;i++)
        dp[i][i]=0;
    for(register int len=2;len<=m;len++)
        for(register int i=1;i+len-1<=m;i++)
        {
            int j=i+len-1;
            if(a[i]==a[j])
            {
                if(len==2)dp[i][j]=0;
                else dp[i][j]=min(dp[i][j],dp[i+1][j-1]);
            }
            dp[i][j]=min(dp[i][j],dp[i][j-1]+ins[a[j]]);
            dp[i][j]=min(dp[i][j],dp[i][j-1]+del[a[j]]);
            dp[i][j]=min(dp[i][j],dp[i+1][j]+ins[a[i]]);
            dp[i][j]=min(dp[i][j],dp[i+1][j]+del[a[i]]);
        }
    printf("%d",dp[1][m]);
    return 0;
}

例四 P3205 合唱队

首先套路设 \(dp_{i,j}\) 为区间 \([i,j]\) 的方案数。

然后发现状态设计有问题。

所以再加一维表示最后一个是从哪边进来的。

有一个小坑点:只有一个人时从左边右边进来都一样,只要初始化一边即可。

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=2010;
const int MOD=19650827;

int n;
int a[MAXN],dp[MAXN][MAXN][2];

int main()
{
    scanf("%d",&n);
    for(register int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(register int i=1;i<=n;i++)
        dp[i][i][1]=1;
    for(register int len=1;len<=n;len++)
    {
        for(register int i=1,j=i+len;j<=n;i++,j++)
        {
            if(a[i]<a[i+1])
                dp[i][j][0]+=dp[i+1][j][0]%MOD;
            if(a[i]<a[j])
                dp[i][j][0]+=dp[i+1][j][1]%MOD;
            if(a[j]>a[i])
                dp[i][j][1]+=dp[i][j-1][0]%MOD;
            if(a[j]>a[j-1])
                dp[i][j][1]+=dp[i][j-1][1]%MOD;
        }
    }
    printf("%d",(dp[1][n][0]+dp[1][n][1])%MOD);
    return 0;
}

例五 P1220 关路灯

有了上一题的经验,一上来就可以设 \(dp_{i,j,0/1}\) 表示在区间 \([i,j]\) 内最后一个是 \(i/j\) 的最小代价。

转移也十分套路,按照思路模拟即可。

需要注意的是初始化。

因为给定了初始位置为 c 。

所以 $ dp_{i,i}$ \(=\) \(abs(a_c-a_i)\) \(*(sum_n-w_c)\)

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXM=60;

int a[MAXM],b[MAXM],sum[MAXM],n,m,c;
int dp[MAXM][MAXM][2];

int main()
{
    scanf("%d%d",&n,&c);
    memset(dp,0x3f,sizeof(dp));
    for(register int i=1;i<=n;i++)
    {
        scanf("%d%d",&a[i],&b[i]);
        sum[i]=sum[i-1]+b[i];
    }
    for(register int i=1;i<=n;i++)
        dp[i][i][0]=dp[i][i][1]=abs(a[i]-a[c])*(sum[n]-b[c]);
    for(register int len=2;len<=n;len++)
        for(register int i=1;i+len-1<=n;i++)
        {
            int j=i+len-1;
            dp[i][j][0]=min(dp[i+1][j][0]+(a[i+1]-a[i])*(sum[i]+sum[n]-sum[j]),dp[i+1][j][1]+(a[j]-a[i])*(sum[i]+sum[n]-sum[j]));
            dp[i][j][1]=min(dp[i][j-1][0]+(a[j]-a[i])*(sum[i-1]+sum[n]-sum[j-1]),dp[i][j-1][1]+(a[j]-a[j-1])*(sum[i-1]+sum[n]-sum[j-1]));
        }
    int ans=min(dp[1][n][0],dp[1][n][1]);
    printf("%d",ans);
    return 0;
}

三.树形DP:

1.定义:

在一颗树形结构上做DP,一般题目会给一颗树形结构

2.步骤:

(1)确定根节点做dfs

(2)设计好状态

(3)找好状态从哪里转移过来,根据题目考虑清楚递归和转移的顺序

(4)推出状态转移方程并转移

(5)在主函数里做dfs即可

3.大概思路

其实本质上跟普通DP一样,就是将大问题转化为多个子问题并递归求解,只不过是在一棵树上做DP,所以要注意的点不同

典型例题

例一 P2015 二叉苹果树

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=105;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

struct node
{
    int to,nxt,len;
}e[MAXN<<2];

int head[MAXN<<2],cnt;
int n,q;
int dp[MAXN][MAXN];

inline void add(int x,int y,int z)
{
    e[++cnt].to=y;
    e[cnt].len=z;
    e[cnt].nxt=head[x];
    head[x]=cnt;
    return;
}

inline int dfs(int x,int fa)
{
    int sum=0;
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int w=e[i].len,y=e[i].to;
        if(y==fa)continue;
        sum=sum+dfs(y,x)+1;
        for(register int v=min(sum,q);v>=0;v--)
            for(register int k=0;k<v;k++)
                dp[x][v]=max(dp[x][v],dp[x][v-k-1]+dp[y][k]+w);
    }
    return sum;
}

int main()
{
    n=read(),q=read();
    for(register int i=1;i<n;i++)
    {
        int x=read(),y=read(),z=read();
        add(x,y,z);
        add(y,x,z);
    }
    dfs(1,0);
    printf("%d",dp[1][q]);
    return 0;
}

可以说是最经典,最基础的树形DP题......

例二 P1131 时态同步

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+5;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

struct node
{
    int to,nxt,len;
}e[MAXN<<1];

int head[MAXN<<1],cnt;
int n,rt,dp[MAXN],dis[MAXN];

inline void add(int x,int y,int z)
{
    e[++cnt].len=z;
    e[cnt].to=y;
    e[cnt].nxt=head[x];
    head[x]=cnt;
    return;
}

inline void dfs1(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(y==fa)continue;
        dfs1(y,x);
        dis[x]=max(dis[x],dis[y]+z);
    }
    return;
}

inline void dfs2(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(y==fa)continue;
        dfs2(y,x);
        dp[x]+=dis[x]-z-dis[y];
    }
    return;
}

signed main()
{
    n=read(),rt=read();
    for(register int i=1;i<n;i++)
    {
        int x=read(),y=read(),z=read();
        add(x,y,z);
        add(y,x,z);
    }
    dfs1(rt,0);
    dfs2(rt,0);
    int ans=0;
    for(register int i=1;i<=n;i++)
        ans+=dp[i];
    printf("%lld",ans);
    return 0;
}

普通树形DP......

例三 P1272 重修道路

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=155; 

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int n, p;
int dp[MAXN][MAXN],ru[MAXN],chu[MAXN]; 
vector<int>nbr[MAXN];

inline int dfs(int cur)
{
    int sum=1;
    for(register int i=0;i<nbr[cur].size();i++) 
    {
        int nxt=nbr[cur][i];
        int tmp=dfs(nxt);
        sum=sum+tmp;
        for(register int v=min(sum,p);v>=2;v--) 
            for(register int k=1;k<=min(tmp,v-1);k++)
                dp[cur][v]=min(dp[cur][v],dp[cur][v-k]+dp[nxt][k]-1);
    }
    return sum;
}

int main()
{
    n=read(),p=read(); 
    memset(dp,0x3f,sizeof(dp));
    for(register int i=1;i<=n-1;i++)
    {
        int x=read(),y=read();
        nbr[x].push_back(y);
        chu[x]++;
    }
    for(register int i=1;i<=n;i++) 
        dp[i][1]=chu[i]; 
    dfs(1);
    int ans=dp[1][p];
    for(register int i=2;i<=n;i++)
        ans=min(ans,dp[i][p]+1);
    printf("%d\n",ans);
    return 0;
}

普通树形DP,不说,看代码......

例四 P2014 选课

Code

点击查看代码
#include <bits/stdc++.h>
#define maxn 1000
using namespace std;
int n,m,f[maxn][maxn],head[maxn],cnt;
struct edge
{
    int to,pre; 
}e[maxn];

inline int read()
{
    char a=getchar();
    while(a<'0'||a>'9')
    {
        a=getchar();
    }
    int t=0;
    while(a>='0'&&a<='9')
    {
        t=(t<<1)+(t<<3)+a-'0';
        a=getchar();
    }
    return t;
}

void add(int from,int to)
{
    e[++cnt].pre=head[from];
    e[cnt].to=to;
    head[from]=cnt;
}

void dp(int now)
{
    for(int i=head[now];i;i=e[i].pre)
    {
        int go=e[i].to;
        dp(go);
        for(int j=m+1;j>=1;j--)
        {
            for(int k=0;k<j;k++)
            {
                f[now][j]=max(f[now][j],f[go][k]+f[now][j-k]);
            }
        }
    }
}

int main()
{
    n=read(),m=read();
    for(int i=1;i<=n;i++)
    {
        int fa=read();
        f[i][1]=read();
        add(fa,i);
    }
    dp(0);
    printf("%d\n",f[0][m+1]);
    return 0;
}

这也没什么好说的,就是普通树形DP......

例五 P5658 括号树

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+5;

struct node
{
    int to,nxt;
}e[MAXN];

int n;

int head[MAXN],cnt;

int ye[MAXN],sum[MAXN],fa[MAXN];

string s;

stack<int>q;

inline void add(int x,int y)
{
    e[++cnt].to=y;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}

inline void dfs(int x)
{
    bool flag=0;
    int tmp;
    if(s[x]==')')
    {
        if(!q.empty())
        {
            flag=1;
            tmp=q.top();
            ye[x]=ye[fa[q.top()]]+1;
            q.pop();
        }
    }
    else if(s[x]=='(')q.push(x);
    sum[x]=sum[fa[x]]+ye[x];
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        dfs(y);
    }
    if(flag!=0)q.push(tmp);
    else if(!q.empty())q.pop();
}

signed main()
{
    scanf("%lld",&n);
    cin>>s;
    s='-'+s;
    for(register int i=2;i<=n;i++)
    {
        int f;
        scanf("%lld",&f);
        add(f,i);
        fa[i]=f;
    }
    dfs(1);
    int ans=0;
    for(register int i=1;i<=n;i++)
        ans^=sum[i]*i;
    printf("%lld",ans);
    return 0;
}

明显括号匹配问题都要用栈好吗......

这道题用到一些小技巧,与普通树形DP略有区别,但思路相同

其实树形DP不难,将本质搞清楚,多写题,将模式弄清楚就真的不难

四.换根DP:

1.定义:

对于一类树形DP,若节点不确定,且答案会随着根节点的不同而变换,这种树形DP可以称之为换根DP

在题目没有指明根节点的情况下在树上做DP,需要有两个dfs,一个普通树形DP,一个用来切换根节点求解

2.状态:

一般的,定义 \(dp_i\)表示以\(i\)为根节点的子树求出的子问题的答案,定义\(f_i\)为以\(i\)为全局根节点的子树求出的子问题的答案

3.状态转移:

一般做两遍dfs

dfs1中做普通树形DP,通过递归求解\(dp_i\)的值,需先递归再转移

dfs2中通过递归来求解换根DP\(f_i\)的值,需先转移再递归

且一般令\(dp_i\)=\(f_i\)

4.注意事项:

换根DP转移时需考虑清楚更换根节点后有哪些变化,增加了哪一部分,减少了哪一部分,把这些想清楚后再来规划转移方程

典型例题

例一 P3478 STA-Station

Code

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e6+5;

struct node
{
    int nxt,to;
}e[MAXN];

int n;
int head[MAXN],cnt;

inline void add(int x,int y)
{
    e[++cnt].to=y;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}

int dep[MAXN],siz[MAXN];

inline void dfs1(int x,int fa)
{
    dep[x]=dep[fa]+1;
    siz[x]=1;
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa)
            continue;
        dfs1(y,x);
        siz[x]+=siz[y];
    }
}

int f[MAXN];

inline void dfs2(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa)
            continue;
        f[y]=f[x]+n-siz[y]*2;
        dfs2(y,x);
    }
}

signed main()
{
    scanf("%lld",&n);
    for(register int i=1;i<=n-1;i++)
    {
        int x,y;
        scanf("%lld%lld",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs1(1,0);
    for(register int i=1;i<=n;i++)
        f[1]+=dep[i];
    dfs2(1,0);
    int root=1,maxn=0;
    for(register int i=1;i<=n;i++)
    {
        if(maxn<f[i])
        {
            maxn=f[i];
            root=i;
        }
    }
    printf("%lld",root);
    return 0;
}

换根DP的基础题,很简单......

例二 P2986 Great Cow Gathering G

Code

点击查看代码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;
const int MAXM=1e5+5;

struct node
{
    int nxt,to,len;
}e[MAXN];

int n,a[MAXM],b[MAXM],l[MAXM],c[MAXM];
int head[MAXN],cnt;
int sum;

inline void add(int x,int y,int z)
{
    e[++cnt].to=y;
    e[cnt].len=z;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}

int dp[MAXN],f[MAXN],siz[MAXN];

inline void dfs1(int x,int fa)
{
    siz[x]=c[x];
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(y==fa)
            continue;
        dfs1(y,x);
        siz[x]+=siz[y];
        dp[x]=dp[x]+dp[y]+siz[y]*z;
    }
}

inline void dfs2(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(y==fa)
            continue;
        f[y]=f[x]+(sum-siz[y])*z-siz[y]*z;
        dfs2(y,x);
    }
}

signed main()
{
    scanf("%lld",&n);
    for(register int i=1;i<=n;i++)
    {
        scanf("%lld",&c[i]);
        sum+=c[i];
    }
    for(register int i=1;i<=n-1;i++)
    {
        scanf("%lld%lld%lld",&a[i],&b[i],&l[i]);
        add(a[i],b[i],l[i]);
        add(b[i],a[i],l[i]);
    }
    dfs1(1,0);
    f[1]=dp[1];
    dfs2(1,0);
    int minn=0x7f7f7f7f7f7f;
    for(register int i=1;i<=n;i++)
        minn=min(minn,f[i]);
    printf("%lld",minn);
    return 0;
}

很基础的一道换根DP,挺水的,只需要搞清楚状态从哪里转移,考虑全面就行了

例三 POJ3585 Accumulation Degree

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;

int dp[MAXN], f[MAXN], deg[MAXN];
bool vis[MAXN];

struct edge
{
    int to,nxt,len;
}e[MAXN<<1];

int head[MAXN],cnt;
int n,T,root,ans;

inline void add(int x, int y, int z) 
{
    e[++cnt].to=y;
    e[cnt].len=z;
    e[cnt].nxt=head[x];
    head[x]=cnt;
    return;
}

inline void dfs1(int x) 
{
    vis[x]=true; 
    dp[x]=0;  
    for(register int i=head[x];i;i=e[i].nxt) 
    { 
        int y=e[i].to,z=e[i].len;
        if(vis[y]) 
            continue;
        dfs1(y);
        if(deg[y]==1) 
            dp[x]+=z;
        else 
            dp[x]+=min(dp[y],z); 
    }
    return;
}

inline void dfs2(int x) 
{
    vis[x]=true;
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to,z=e[i].len;
        if(vis[y]) 
            continue;
        if(deg[x]==1) 
            f[y]=dp[y]+z;
        else
            f[y]=dp[y]+min(f[x]-min(dp[y],z),z);
        dfs2(y);
    }
    return ;
}

int main() 
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>T;
    while(T--) 
    {
        cnt=1;
        cin>>n;
        for(register int i=1;i<=n;i++)
            head[i]=f[i]=dp[i]=deg[i]=vis[i]=0;
        for(register int i=1;i<=n-1;i++) 
        {
            int x,y,z;
            cin>>x>>y>>z;
            add(x,y,z);
            add(y,x,z);
            deg[x]++;
            deg[y]++;
        }
        int root=1; 
        dfs1(root);
        memset(vis,0,sizeof(vis));
        f[root]=dp[root];
        dfs2(root);
        int ans=0;
        for(register int i=1;i<=n;i++)
            ans=max(ans,f[i]);
        printf("%d\n",ans);
    }
    return 0;
}

这道题主要是要考虑叶子结点,叶子节点必须设为极大值,且用度数统计判叶子节点的话要注意是否为只有一个子节点的根节点,若是则不能设为极大值

例四 CF1187E Tree Painting

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=200005;
struct node
{
    int to,nxt;
}e[MAXN<<1];
int n,cnt;
int head[MAXN],dp[MAXN],siz[MAXN],f[MAXN];
int ans,rea;
inline void up(int x,int y)
{
    dp[y]=dp[x]+n-siz[y]*2;
}
inline void add(int x,int y)
{
    e[++cnt].to=y;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}
inline void dfs1(int x,int fa)
{
    siz[x]=1;
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa)continue;
        dfs1(y,x);
        siz[x]+=siz[y];
        f[x]+=f[y];
    }
    f[x]+=siz[x];
}
inline void dfs2(int x,int fa)
{
    for(register int i=head[x];i;i=e[i].nxt)
    {
        int y=e[i].to;
        if(y==fa)continue;
        up(x,y);
        dfs2(y,x);
    }
}
signed main()
{
    scanf("%lld",&n);
    for(register int i=1;i<n;i++)
    {
        int x,y;
        scanf("%lld%lld",&x,&y);
        add(x,y);
        add(y,x);
    }
    dfs1(1,0);
    dp[1]=f[1];
    dfs2(1,0);
    for(register int i=1;i<=n;i++)
        ans=max(ans,dp[i]);
    printf("%lld",ans);
    return 0;
}

其实也挺水的,只要理解了就好做,跟上面那题差不太多......

其实换根DP本身不难,只是有些题目会特意把它包装得很恶心,所以要注意细节

五.状压DP:

1.定义:

当状态的维度很多,而每一个维度的取值是布尔值时(如dp[2][2]...),则可以用二进制数值去表示一个状态,这种DP称作状压DP

2.基本的位运算:(优先级:四则运算>位运算)

(1) 按位与(&):两个整数在二进制下逐位比较,同一位有2个1,则结果为1,否则为0

(2) 按位或(|):两个整数在二进制下逐位比较,同一位有1,则答案为1,否则为0

(3) 按位异或(^/\(xor\)):两个整数在二进制下逐位比较,同一位上不同,则答案为1,否则为0

(4) 按位取反(~):一个整数在二进制下逐位取反

3.位移操作:

\(x\)<<\(y\)\(x*2^y\)

\(x\)>>\(y\)\(x/2^y\)

4.常用位操作意义:(重点)

  • 一个二进制数 &1 得本身

  • 一个二进制数 ^1 取反

  • 一个二进制数 &0 为0

  • 一个二进制数 |1 为1

  • (n>>k)&1 取出二进制下n的第k位(从左往右)

  • n&((1<<k)-1) 取出二进制下n的右k位

  • n^(1<<k) 将二进制下n的第k位取反

  • n|(1<<k) 将二进制下n的第k位赋值1

  • n&(~(1<<k)) 将二进制下n的第k位赋值0

典型例题

例一 OpenJ_Bailian4124 海贼王之伟大航路

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=17;

int n;
int dp[1<<MAXN][MAXN];
int a[MAXN][MAXN];

int main()
{
    scanf("%d",&n);
    for(register int i=1;i<=n;i++)
    {
        for(register int j=1;j<=n;j++)
        {
            scanf("%d",&a[i][j]);
        }
    }
    memset(dp,0x7f,sizeof(dp));
    dp[1][1]=0;
    for(register int i=1;i<=(1<<n)-1;i++)//枚举所有经过的状态
        for(register int j=1;j<=n;j++)//枚举当前到达的点
        {
            if(!((i>>(j-1))&1))
                continue;//i的第j为0,无效的状态 
            for(register int k=1;k<=n;k++)
            {
                if((i>>(k-1))&1)//第k个点已经经过 
                    dp[i][j]=min(dp[i][j],dp[i^(1<<(j-1))][k]+a[k][j]);
            }
        } 
    printf("%d\n",dp[(1<<n)-1][n]);
    return 0;
}

这题作为状压DP的基础题,还是不难理解的,思路在注释中已体现

例二 P1171 售货员的难题

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=20;

int dp[1<<MAXN][MAXN],w[MAXN][MAXN];
int n;

int main()
{
    scanf("%d",&n);
    for(register int i=0;i<=n-1;i++)
        for(register int j=0;j<=n-1;j++)
            scanf("%d",&w[i][j]);
    memset(dp,0x3f,sizeof(dp));
    dp[1][0]=0;
    for(register int i=0;i<(1<<n);i++)//枚举所有经过的状态 
        for(register int j=0;j<=n-1;j++)//枚举当前到达的点
        {
            if(!((i>>j)&1))
                continue;//i的第j为0,无效的状态 
            for(register int k=0;k<=n-1;k++)
            {
                if(((i>>k)&1)&&j!=k)//第k个点已经经过 
                    dp[i][j]=min(dp[i][j],dp[i^(1<<j)][k]+w[k][j]);
            }
        } 
    int minn=0x7f7f7f;
    for(register int i=0;i<n;i++)
    {
        minn=min(minn,dp[(1<<n)-1][i]+w[i][0]);
    }
    printf("%d\n",minn);
    return 0;
}

这道题和上一题区别不大,只不过最后要统计终点到各村庄的距离并求min,但是这题很恶心的一点就是,又卡空间又卡常数,所以空间不能开太大,并且时间方面也要做些优化。

例三 P1879 Corn Fields G

这道题可以不用状压,但用状压来练手
\(dp_{i,j}\)表示前i行且第i行种草的状态是j的二进制的方案数。
输出为\(cout<<\sum\limits^{i=1}_{i<=m}dp_{m,i}\)

有三个特判:

1.若种在了贫瘠土地上,则不行,若是\(i\&j\)等于本身的状态,则没有贫瘠的土地,否则跳过。

2.判断是否有相邻,若是\(i\&(i<<1)!=0\),则有相邻,则跳过。

3.上下与左右相同。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=13;
const int MOD=1e9;

int m,n;

int soil[MAXN+5],a[MAXN+5][MAXN+5],dp[MAXN][1<<MAXN];

int main()
{
    scanf("%d%d",&m,&n);
    for(register int i=1;i<=m;i++)
    {
        for(register int j=1;j<=n;j++)
        {
            scanf("%d",&a[i][j]);
            soil[i]=(soil[i]<<1)+a[i][j];
        }
    }
    dp[0][0]=1;
    for(register int i=1;i<=m;i++)
        for(register int j=0;j<(1<<n);j++)
        {
            if((j&soil[i])!=j)
                continue;
            if((j&(j<<1)))
                continue;
            for(register int k=0;k<(1<<n);k++)
            {
                if(!(j&k))
                    dp[i][j]=(dp[i][j]+dp[i-1][k])%MOD;
            }
        }
    int ans=0;
    for(register int i=0;i<(1<<n);i++)
        ans=(ans+dp[m][i])%MOD;
    printf("%d\n",ans);
    return 0;
}

例四 P2704 炮兵阵地

这道题需在最开始将无用状态筛掉,然后还有因为这里需要处理上行和上上行,所以要多加一个维度。

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=10;

int num[1<<MAXN];
int cnt,sta[1<<MAXN];
int dp[MAXN*MAXN+5][200][200];
int a[MAXN*MAXN+5];

inline int getsum(int x)
{
    int xx=0;
    while(x)
    {
        if((x&1))
            xx++;
        x>>=1;
    }
    return xx;
}

int n,m;

int main()
{
    scanf("%d%d",&n,&m);
    for(register int i=1;i<=n;i++)
    {
        for(register int j=0;j<m;j++)
        {
            char op;
            cin>>op;
            if(op=='P')
                a[i]=(a[i]<<1)+1;
            else
                a[i]=(a[i]<<1); 
        } 
    }
    for(register int i=0;i<(1<<m);i++)
    {
        if((i&(i<<1))||(i&(i<<2)))
            continue;
        cnt++;
        sta[cnt]=i;
        num[cnt]=getsum(i);
    }
    dp[0][1][1]=0;
    for(register int i=1;i<=n;i++)
    {
        for(register int j=1;j<=cnt;j++)
        {
            if((sta[j]&a[i])!=sta[j])
                continue;
            for(register int k=1;k<=cnt;k++)
            {
                if(sta[j]&sta[k])
                    continue;
                for(register int l=1;l<=cnt;l++)
                {
                    if(sta[j]&sta[l])
                        continue;
                    if(sta[k]&sta[l])
                        continue;
                    dp[i][j][k]=max(dp[i][j][k],dp[i-1][k][l]+num[j]);
                }
            }
        }
    }
    int ans=0;
    for(register int i=1;i<=cnt;i++)
    {
        for(register int j=1;j<=cnt;j++)
        {
            ans=max(ans,dp[n][i][j]);
        }
    }
    printf("%d\n",ans);
    return 0;
}

例五 SP1700 TRSTAGE - Traveling by Stagecoach

\(dp_{i,j}\)表示当前位置为j且车票使用的状态为二进制下的i。

输出for(register int i=0;i<(1<<n);i++)\(dp_{i,b}\)的最小值

状态转移方程:

\(dp_{i,j}=min(dp_{i,j},dp_{i \^(i<<k) ,last}+dis \times 1.0 / t_{k+1})\)

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=35;
const int MAXN=9; 
const int MAXM=1024;

struct Node
{
    int to,nxt;
    double len;
}e[MAXM];

int head[MAXM],cnt;

inline void add(int x,int y,int z)
{
    e[++cnt].to=y;
    e[cnt].len=z;
    e[cnt].nxt=head[x];
    head[x]=cnt;
}

int n,m;
int p,a,b;
double t[MAXN+5];
double dp[1<<MAXN][N];

int main()
{
    while(scanf("%d%d%d%d%d",&n,&m,&p,&a,&b))
    {
        if(!n&&!m&&!p&&!a&&!b)
            return 0;
        cnt=0;
        memset(head,0,sizeof(head));
        for(register int i=0;i<(1<<n);i++)
        {
            for(register int j=1;j<=m;j++)
            {
                dp[i][j]=1e9;
            }
        }
        for(register int i=1;i<=n;i++)
            scanf("%lf",&t[i]);
        for(register int i=1;i<=p;i++)
        {
            int x,y;
            double z;
            scanf("%d%d%lf",&x,&y,&z);
            add(x,y,z);
            add(y,x,z);
        }
        dp[0][a]=0;
        for(register int i=0;i<(1<<n);i++)
        {
            for(register int j=0;j<n;j++)
            {
                if(!(i&(1<<j)))
                    continue;
                for(register int x=1;x<=m;x++)
                {
                    for(register int l=head[x];l;l=e[l].nxt)
                    {
                        int y=e[l].to;
                        double z=e[l].len;
                        dp[i][x]=min(dp[i][x],dp[i^(1<<j)][y]+z*1.0/t[j+1]);
                    }
                }
            }
        }
        double minn=1e9;
        for(register int i=0;i<(1<<n);i++)
        {
            minn=min(minn,dp[i][b]);
        }
        if(minn!=1e9)
        {
            printf("%.3lf\n",minn);
        }
        else
        {
            printf("Impossible\n");
        }
    }
    return 0;
}

例六 P3092 No Change G

dp[i]表示花的钱的状态为二进制下的i的最多的物品数量。

输出:

for(register i=0;i<(1<<k);i++)
    if(dp[i]==n)
        maxi=max(maxi,剩余硬币面值和);

状态转移:

for(register int i=0;i<(1<<k);i++)
    for(register int j=0;j<k;j++)//枚举当次用掉的硬币
        if((i>>j)&1)
            二分

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e5+5;

int k,n;
int t[MAXN],a[MAXN];
int dp[1<<17];
int sum[MAXN];

signed main()
{
    scanf("%lld%lld",&k,&n);
    for(register int i=0;i<k;i++)
        scanf("%lld",&t[i]);
    for(register int i=1;i<=n;i++)
    {
        scanf("%lld",&a[i]);
        sum[i]=sum[i-1]+a[i];
    }
    for(register int i=0;i<(1<<k);i++)
    {
        for(register int j=0;j<k;j++)
        {
            if(!(i&(1<<j)))
                continue;
            int la=dp[i^(1<<j)];
            int l=1,r=n,now=0;
            while(l<=r)
            {
                int mid=(l+r)>>1;
                if(sum[mid]<=sum[la]+t[j])
                    l=mid+1,now=mid;
                else 
                    r=mid-1;
            }
            dp[i]=max(dp[i],now);
        }
    }
    int ans=-1;
    for(register int i=0;i<(1<<k);i++)
    {
        if(dp[i]!=n)
            continue;
        int cnt=0;
        for(register int j=0;j<k;j++)
            if(!(i&(1<<j)))
                cnt+=t[j];
        ans=max(ans,cnt);
    }
    printf("%lld",ans);
    return 0;
}

总的来说,状压DP并不难理解,难就难在位运算弄不清楚,所以要多熟悉各种位运算的技巧(假)
哎呀状压终于完了,我真的被恶心的得上吐下泻,五脏六腑都翻江倒海...(真)

六.数据结构优化DP:

1.常见的数据结构:

线段树优化DP
树状数组优化DP
单调队列优化DP
单调栈优化DP

典型例题

例一 UVA12983 The Battle of Chibi

树状数组优化DP

\(dp_{i,j}\)表示以\(j\)为结尾,且长度为\(i\)的严格上升子序列的个数

输出:

for(register int i=1;i<=n;i++)
	ans=(ans+dp[m][i])%MOD;

状态转移:用树状数组优化(详见代码)

Code

点击查看代码
#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
const int MOD=1e9+7;

int n,m;
int c[MAXN];
int a[MAXN],b[MAXN];
int dp[MAXN][MAXN];//结尾为i,且长度为j的严格上升子序列的个数 

inline int lowbit(int x)
{
    return x&-x;
}

inline void add(int x,int k)//树状数组修改操作 
{
    for(register int i=x;i<=n;i+=lowbit(i))
        c[i]=(c[i]+k)%MOD;
    return;
}

inline int ask(int x)//树状数组询问操作 
{
    int ans=0;
    for(register int i=x;i>=1;i-=lowbit(i))
        ans=(ans+c[i])%MOD;
    return ans;
}

inline void dis()//离散化 
{
    memcpy(b,a,sizeof(b));//将a复制到b
    sort(b+1,b+n+1);//用unique去重则需要排序
    int len=unique(b+1,b+n+1)-(b+1);
    for(register int i=1;i<=n;i++)
        a[i]=lower_bound(b+1,b+len+1,a[i])-b;//返回的地址-a数组的首地址=离散化的值 
    return;
}

inline void clear()//初始化 
{
    memset(dp,0,sizeof(dp));
    memset(c,0,sizeof(c));
    return;
}

inline int solve()//dp过程 
{
    dis();
    for(register int i=1;i<=n;i++)
        dp[1][i]=1;
    for(register int i=2;i<=m;i++)
    {
        for(register int j=1;j<=n;j++)
        {
            add(a[j],dp[i-1][j]);
            dp[i][j]=ask(a[j]-1);
        }
        memset(c,0,sizeof(c));
    } 
    int ans=0;
    for(register int i=1;i<=n;i++)
        ans=(ans+dp[m][i])%MOD;
    return ans;
}

int t;

int main()
{
    scanf("%d",&t);
    for(register int i=1;i<=t;i++)
    {
        scanf("%d%d",&n,&m);
        for(register int j=1;j<=n;j++)
            scanf("%d",&a[j]);
        clear();
        printf("Case #");//按格式输出 
        printf("%d",i);
        printf(": ");
        printf("%d",solve());
        puts("");
    }
    return 0;
}

例二 P4644 Cleaning Shifts S

题目大意
1.从\([m,e]\)要全部覆盖,可以重叠;
2.n头牛,每头牛打扫区间\([l_i,r_i]\)工资为\(s_i\),只要雇佣,就必须付\(s_i\)的工资。
目标:最小花费

策略
1.每头牛打扫整个区间,贪心;
2.\(dp_{r_i}\)表示打扫从\(M\)开始到\(i\)的区间的最小花费;
3.将牛按照右边界\(r_i\)排序。

Code(没优化版本)

点击查看代码

这里可以虚拟一个0号牛,然后后面转移从0开始

#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e4+5;

inline int read()
{
    register int x=0,f=0;register char ch=getchar();
    while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return f?-x:x;
}

struct Node
{
    int l,r,s;
}node[MAXN];

int n,m,e;
int dp[MAXN*10];

inline bool cmp(Node a,Node b)
{
    if(a.r==b.r)
        return a.l<b.l;
    return a.r<b.r;
}

signed main()
{
    scanf("%lld%lld%lld",&n,&m,&e);
    for(register int i=1;i<=n;i++)
    {
        scanf("%lld%lld%lld",&node[i].l,&node[i].r,&node[i].s);
    }
    memset(dp,0x7f,sizeof(dp));
    sort(node+1,node+n+1,cmp);
    node[0].r=m;
    dp[m]=0;
    for(register int i=1;i<=n;i++)
    {
        for(register int j=0;j<i;j++)
        {
            if(node[j].r>=node[i].l-1)
            {
                dp[node[i].r]=min(dp[node[i].r],dp[node[j].r]+node[i].s);
            }
        }
    }
    int ans=1e9;
    for(register int i=1;i<=n;i++)
    {
        if(node[i].r>=e)
            ans=min(ans,dp[node[i].r]);
    }
    if(ans==1e9)
    {
        printf("-1\n");
    }
    else
    {
        printf("%lld",ans);
    }
    return 0;
}

很明显,这样暴力的做法是\(O(n^2)\),这道题数据水不说,但是一般会给\(10^5\)以上,所以就会超时,所以我们需要进行一些优化。

然而看到我们转移的这里
dp[node[i].r]=min(dp[node[i].r],dp[node[j].r]+node[i].s);
很明显node[i].s是不变的,所以要结果尽可能小的话,只需要dp[node[j].r]尽可能小就可以了,所以我们这里用线段树来查询区间最小值。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;

inline int read()
{
    register int x=0,f=0;register char ch=getchar();
    while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return f?-x:x;
}

struct Node
{
    int l,r,s;
}node[MAXN];

int n,m,e;
int dp[MAXN*10];

inline bool cmp(Node a,Node b)
{
    if(a.r==b.r)
        return a.l<b.l;
    return a.r<b.r;
}

int t[MAXN],dat[MAXN];

inline void pushup(int p)
{
    t[p]=min(t[p<<1],t[p<<1|1]);
}

inline void build(int p,int l,int r)
{
    if(l==r)
    {
        t[p]=dp[l];
        return;
    }
    int mid=(l+r)>>1;
    build(p<<1,l,mid);
    build(p<<1|1,mid+1,r);
    pushup(p);
}

inline void change(int p,int l,int r,int a,int b,int k)
{
    if(l>b||r<a)
        return;
    if(l>=a&&r<=b)
    {
        t[p]=k;
        return;
    }
    int mid=(l+r)>>1;
    change(p<<1,l,mid,a,b,k);
    change(p<<1|1,mid+1,r,a,b,k);
    pushup(p);
}

inline int ask(int p,int l,int r,int a,int b)
{
    if(l>b||r<a)
        return 0x7f7f7f7f;
    if(l>=a&&r<=b)
        return t[p];
    int mid=(l+r)>>1;
    return min(ask(p<<1,l,mid,a,b),ask(p<<1|1,mid+1,r,a,b));
}

signed main()
{
    scanf("%lld%lld%lld",&n,&m,&e);
    for(register int i=1;i<=n;i++)
    {
        scanf("%lld%lld%lld",&node[i].l,&node[i].r,&node[i].s);
    }
    memset(dp,0x7f,sizeof(dp));
    sort(node+1,node+n+1,cmp);
    dp[m]=0;
    build(1,m,e);
    int ans=1e9;
    for(register int i=1;i<=n;i++)
    {
        int op=ask(1,m,e,node[i].l-1,node[i].r-1);
        dp[node[i].r]=min(dp[node[i].r],op+node[i].s);
        change(1,m,e,node[i].r,node[i].r,dp[node[i].r]); 
        if(node[i].r>=e)
            ans=min(ans,dp[node[i].r]);
    }
    if(ans==1e9)
    {
        printf("-1\n");
    }
    else
    {
        printf("%lld",ans);
    }
    return 0;
}

例三 POJ2374 Fence Obstacle Course(题面翻译)

这题其实并不难,暴力正确性是可以保证的,但时间复杂度是\(O(n^2)\),这\(10^5\)的数据明显会超时,所以需要加一些优化。
这里可以设\(dp_{i,0/1}\)表示走到第\(i\)个栅栏时所用的路程,\(0\)表示走到第\(i\)个时在右侧,\(1\)表示在左侧。
我们可以将这个过程反过来,假设是从\(*\)走到\(s\)
我们可以这样想,每次往上走时肯定是碰到一个比当前最长的栅栏更长的栅栏然后才要横向走,我们可以将这些栅栏看成一些区间,则可以用单调栈来做一个区间查询快速找到下一个栏杆来优化,就将\(O(n^2)\)转化为\(O(n \times logn)\),就不会超时了。

例四 P4954 Tower of Hay G

单调队列优化DP

一般形式是 \(dp_i=\min\begin{Bmatrix} dp_i,dp_j+A \times a_i+B\times a_j+C\end{Bmatrix}\)

分析:

1.从前往后划分,会导致最后可能有干草包不能堆叠到塔上,所以从前往后划分;

2.\(dp_i\) 表示从塔顶到第 \(i\) 堆干草堆出的最大高度;

3.\(w_i\) 表示前 \(i\) 堆草堆出的最大高度为 \(dp_i\) 时,最后一层的干草的宽度,

if(sum[i]-sum[j]>=w[j])
    dp[i]=max(dp[i],dp[j]+1);
w[i]=sum[i]-sum[j];

4.要使得 \(dp_i\) 尽量的大,等价于要使得 \(w_i\) 尽量的小,等价于 \(sum_j\) 要尽量的大。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;

int w[MAXN],sum[MAXN],q[MAXN],dp[MAXN];
int a[MAXN];
int n;

inline int calc(int x)
{
    return w[x]+sum[x];
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=n;i>=1;i--)
        cin>>a[i];
    for(register int i=1;i<=n;i++)
        sum[i]=sum[i-1]+a[i];
    int l=1,r=0;
    for(register int i=1;i<=n;i++)
    {
        while(l<=r && sum[i]>=calc(q[l]))
            l++;
        w[i]+=sum[i]-sum[q[l-1]];
        dp[i]=dp[q[l-1]]+1;
        while(l<=r && calc(i)<=calc(q[r]))
            r--;
        q[++r]=i;
    }
    printf("%d\n",dp[n]);
    return 0;
}

例五 POJ2373 Dividing the Path

\(dp_i\) 表示浇到第 \(i\) 的位置时,所用的洒水器的最小数量,

\[dp_i=\min\begin{Bmatrix}dp_i,dp_j+1(i-2b\leqslant j \leqslant i-2a) \end{Bmatrix} \]

那么我们现在的时间复杂度是 \(O(n·L)\) 的,所以我们考虑用单调队列优化。

显然,我们维护的 \(j\) 是在一个 \(i-2b\)\(i-2a\) 的滑动窗口,那么我们每次将 \(dp_{i-2a}\) 入队,然后这就是一个 \(j\)\(dp_{j}\) 都递增的单调队列,就可以做了。

Code

点击查看代码
#include<iostream>
#include<string.h>
using namespace std;
const int MAXN=1e6+5;
const int INF=0x7f7f7f7f;

int dp[MAXN];
int n,L;
int a,b;
int s,e;
int q[MAXN],l,r;
bool vis[MAXN];

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>L;
    cin>>a>>b;
    for(register int i=1;i<=n;i++)
    {
        cin>>s>>e;
        for(register int i=s+1;i<=e-1;i++)
            vis[i]=true;
    }
    memset(dp,INF,sizeof(dp));
    dp[0]=0;
    int l=1,r=0;
    q[++r]=0;
    for(register int i=a*2;i<=L;i+=2)
    {
        while(l<=r && q[l]<i-2*b)
            l++;
        while(l<=r && dp[q[r]]>=dp[i-2*a])
            r--;
        q[++r]=i-2*a;
        if(vis[i])
            continue;
        int j=q[l];
        dp[i]=dp[j]+1;
    }
    if(dp[L]<INF)
        printf("%d\n",dp[L]);
    else
        printf("-1\n");
	return 0;
}

例六 P2569 股票交易

\(dp_{i,j}\) 表示第 \(i\) 天后,拥有 \(j\) 张股票的最大获得钱数。

则有四种情况:

首先全部设为 INF,

1.什么都没有就来买

\[dp_{i,j}=-ap_i\times j(0 \leqslant j \leqslant as_i) \]

2.按兵不动,不买不卖

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-1,j}\end{Bmatrix} \]

3.在之前有的基础上买股票

那么上次交易至少是在第 \(i-w-1\) 天,假设第 \(i-w-1\) 天拥有 \(k\) 张股票,\(k\) 一定比 \(j\) 小,但股票有底线,最少为 \(j-as_i\)

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}-(j-k)\times ap_i\end{Bmatrix}(j-as_i \leqslant k < j) \]

4.在之前有的基础上卖股票

假设第 \(i-w-1\) 天拥有 \(k\) 张股票,那么 \(k\) 一定比 \(j\) 大,但股票有上限,最多为 \(j+bs_i\)

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}+(k-j)\times bp_i\end{Bmatrix}(j < k \leqslant j+bs_i) \]

然后我们现在的这个朴素DP是三次方级别的,那我们想优化:

对于第3、4种情况,\(k\) 的范围很明显是符合滑动窗口的特征的,那么我们就用单调队列来维护一下。

对于第3、4种情况,我们分别改变一下它的式子

第三种:

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}-j\times ap_i+k\times ap_i\end{Bmatrix}(j-as_i \leqslant k < j) \]

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}+k\times ap_i\end{Bmatrix}-j\times ap_i(j-as_i \leqslant k < j) \]

第四种:

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}-j\times bp_i+k\times bp_i\end{Bmatrix}(j < k \leqslant j+bs_i) \]

\[dp_{i,j}=\max\begin{Bmatrix}dp_{i,j},dp_{i-w-1,k}+k\times bp_i\end{Bmatrix}-j\times bp_i(j < k \leqslant j+bs_i) \]

那么这个时候就能够用单调队列来优化。

而且还要注意一点,情况3要顺序循环,而情况4要逆序循环。

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e3+5;
const int INF=0x7f7f7f7f;

int t,maxp,w;
int ap[MAXN],bp[MAXN],as[MAXN],bs[MAXN];
int q[MAXN<<3];
int dp[MAXN][MAXN];
int l,r;

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t>>maxp>>w;
    for(register int i=1;i<=t;i++)
        cin>>ap[i]>>bp[i]>>as[i]>>bs[i];
    memset(dp,-INF,sizeof(dp));
    for(register int i=0;i<=t;i++)
        dp[i][0]=0;
    for(register int i=1;i<=t;i++)
    {
        for(register int j=0;j<=as[i];j++)
            dp[i][j]=-ap[i]*j;
        for(register int j=maxp;j>=0;j--)
            dp[i][j]=max(dp[i][j],dp[i-1][j]);
        if(i-w-1>=0)
        {
            l=1,r=0;
            for(register int j=0;j<=maxp;j++)
            {
                while(l<=r && q[l]<j-as[i])
                    l++;
                while(l<=r && dp[i-w-1][j]+ap[i]*j>=dp[i-w-1][q[r]]+ap[i]*q[r])
                    r--;
                q[++r]=j;
                if(l<=r)
                    dp[i][j]=max(dp[i][j],dp[i-w-1][q[l]]-ap[i]*(j-q[l])); 
            } 
            l=1,r=0;
            for(register int j=maxp;j>=0;j--)
            {
                while(l<=r && q[l]>j+bs[i])
                    l++;
                while(l<=r && dp[i-w-1][j]+bp[i]*j>=dp[i-w-1][q[r]]+bp[i]*q[r])
                    r--;
                q[++r]=j;
                if(l<=r)
                    dp[i][j]=max(dp[i][j],dp[i-w-1][q[l]]+bp[i]*(q[l]-j));
            } 
        } 
    }
    int ans=0;
    for(register int i=0;i<=maxp;i++)
        ans=max(dp[t][i],ans);
    printf("%d",ans);
    return 0;
}

七.一些奇怪的DP:

典型例题

例一 AT2330 Mixing Experiment

定义dp[i][j]表示A元素含量为i,B元素含量为j的最小代价

1.输出:

for(i)
    for(j)
        if(i*mb==j*ma)
            mini=min(mini,dp[i][j]);

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=405;

int n,ma,mb;
int a[MAXN],b[MAXN],c[MAXN];
int dp[MAXN][MAXN];
int suma,sumb;

int main()
{
    for(register int i=0;i<MAXN;i++)
        for(register int j=0;j<MAXN;j++)
            dp[i][j]=1e9;
    scanf("%d%d%d",&n,&ma,&mb);
    for(register int i=1;i<=n;i++)
    {
        scanf("%d%d%d",&a[i],&b[i],&c[i]);
        suma+=a[i];
        sumb+=b[i];
    }
    dp[0][0]=0;
    for(register int i=1;i<=n;i++)
    {
        for(register int j=suma;j>=a[i];j--)
        {
            for(register int k=sumb;k>=b[i];k--)
            {
                dp[j][k]=min(dp[j][k],dp[j-a[i]][k-b[i]]+c[i]);
            }
        }
    }
    int mini=1e9;
    for(register int i=1;i<=suma;i++)
    {
        for(register int j=1;j<=sumb;j++)
        {
            if(i*mb==j*ma)
                mini=min(mini,dp[i][j]);
        }
    }
    if(mini!=1e9)
        printf("%d",mini);
    else
        printf("-1");
    return 0;
}

例二 CF577B Modulo Sum

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e3+5;
int n,m;
bool dp[MAXN][MAXN];
int a[MAXN];
int main()
{
    scanf("%d%d",&n,&m);
    if(n>m)
    {
        puts("YES");
        return 0;
    }
    for(register int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        a[i]%=m;
        dp[i][a[i]]=1;
        if(!a[i])
        {
            puts("YES");
            return 0;
        }
    }
    for(register int i=1;i<=n;i++)
    {
        for(register int j=0;j<m;j++)
        {
            dp[i][j]|=dp[i-1][j];
            dp[i][(j+a[i])%m]|=dp[i-1][j];
        }
        if(dp[i][0])
        {
            puts("YES");
            return 0;
        }
    }
    puts("NO");
    return 0;
}

例三 P1441 砝码称重

这道题可以用状压来做,我们搞一个小bitset...

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e3+5;
const int MAXM=20;

inline int read()
{
    register int x=0,f=0;register char ch=getchar();
    while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return f?-x:x;
}

int n,m;
int a[MAXN];
int ans=-0x7f7f7f;

inline int count(int x)
{
    int ans=0;
    for(register int i=0;i<n;i++)
    {
        if(x&(1<<i))
            ans++;
    }
    return ans;
}

int main()
{
    n=read(),m=read();
    for(register int i=0;i<n;i++)
        a[i]=read();
    for(register int i=0;i<(1<<n);i++)
    {
        if(count(i)==(n-m))
        {
            bitset<MAXN>b;
            b[0]=1;
            for(register int j=0;j<n;j++)
            {
                if(i&(1<<j))    
                    b=b|b<<a[j];
            }
            ans=max(ans,(int)b.count());
        }
    }
    printf("%d",ans-1);
    return 0;
}

例四 CF185A Plant

结论题

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long 
#define MOD 1000000007
using namespace std;

inline int read()
{
    register int x=0,f=0;register char ch=getchar();
    while(ch<'0' || ch>'9')f|=ch=='-',ch=getchar();
    while(ch>='0' && ch<='9')x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
    return f?-x:x;
}

inline int fpow(int p,int ba)
{
    p%=MOD;
    int ans=1;
    while(ba)
    {
        if(ba%2==1)ans=ans*p%MOD;
        p=p*p%MOD;
        ba/=2;
    }
    return ans%MOD;
}

int n;

signed main()
{
    scanf("%lld",&n);
    if(n==0)
    {
        printf("1\n");
        return 0;
    }
    printf("%lld",(fpow(2,n-1)+fpow(2,2*n-1))%MOD);
    return 0;
}

八.数位DP:

1.定义:

数位DP就是解决计数的问题,一般来说,只是预处理需要用到DP
难点:如何不漏统计?如何不重复统计?

2.题目形式

数位DP的题目形式:
\([L,R]\)区间内满足条件的数有多少?

3.解决策略:

1.求出\([1,R]\)\([1,L-1]\)满足条件的数的数量,然后相减;
2.从高位到低位枚举每个数位\(i\),并统计以不超过\(digit[i]\)开头的满足条件的整数数量;
3.预处理\(dp[i][j]\),表示\(i\)位数以\(j\)开头且满足条件的整数的数量;

典型例题

例一 HDU2089 不要62

此题用到DP的地方:

dp[0][0]=1;
for(register int i=1;i<=upper;i++)
    for(register int j=0;j<=9;j++)
        for(register int k=0;k<=9;k++)
            if(j!=4&&!(j==6&&k==2))
                dp[i][j]+=dp[i-1][k];

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=10;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int n,m;
int dp[MAXN][MAXN];

inline void init()
{
    dp[0][0]=1;
    for(register int i=1;i<=7;i++)
        for(register int j=0;j<10;j++)
            for(register int k=0;k<10;k++)
                if((j!=4)&&!(j==6&&k==2))
                    dp[i][j]+=dp[i-1][k];
    return;
}

inline int solve(int now)
{
    vector<int>digit;
    while(now)
    {
        digit.push_back(now%10);
        now/=10;
    }
    digit.push_back(0);
    int ans=0;
    for(register int i=digit.size()-2;i>=0;i--)
    {
        for(register int j=0;j<digit[i];j++)
        {
            if((j!=4)&&!(j==2&&digit[i+1]==6))
                ans+=dp[i+1][j];
        }
        if(digit[i]==4||digit[i]==2&&digit[i+1]==6)
            break;
    }
    return ans;
}

int main()
{
    init();
    while(scanf("%d%d",&n,&m))
    {
        if(n==0&&m==0)
            break;
        printf("%d\n",solve(m+1)-solve(n));
    }
    return 0;
}

例二 HDU5179 beautiful number

同样数位DP题,但是有些许细节

Code'

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=10;

inline int read()
{
    register int x=0,f=1;
    register char ch=getchar();
    while(!isdigit(ch))
    {
        if(ch=='-')
            f=-1;
        ch=getchar();
    }
    while(isdigit(ch))
    {
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar();
    }
    return x*f;
}

int n,m;
int dp[MAXN][MAXN];

inline void init()
{
    for(register int i=1;i<=9;i++)
        dp[1][i]=1;
    for(register int i=2;i<=10;i++)
        for(register int j=1;j<10;j++)
            for(register int k=1;k<=j;k++)
                if(j%k==0)
                    dp[i][j]+=dp[i-1][k];
    return;
}

inline int solve(int now)
{
    vector<int>digit;
    while(now)
    {
        digit.push_back(now%10);
        now/=10;
    }
    int ans=0;
    int len=digit.size();
    for(register int i=1;i<len;i++)
        for(register int j=1;j<=9;j++)
            ans+=dp[i][j];
    for(register int i=1;i<digit[len-1];i++)
        ans+=dp[len][i];
    for(register int i=len-2;i>=0;i--)
    {
        for(register int j=1;j<digit[i];j++)
        {
            if((digit[i+1]!=0)&&(digit[i+1]%j==0))
                ans+=dp[i+1][j];
        }
        if(digit[i]==0||digit[i+1]%digit[i]!=0)
            break;
    }
    return ans;
}

int main()
{
    init();
    int t=read();
    while(t--)
    {
        n=read(),m=read();
        printf("%d\n",solve(m+1)-solve(n));
    }
    return 0;
}

例三 POJ3208 Apocalypse Someday

与一般的数位 \(\text{dp}\) 有点不同的是,没有给出上界,而是要通过值来判断这一位该填什么,当然是从高位向低位填。

为了知道这一位填下去对答案有什么影响,需要预处理出后面无限制的魔鬼数个数。

预处理魔鬼数最重要的是不重不漏。这一位的魔鬼数=上一位的所有魔鬼数+这一位填 \(6\) 带来的新魔鬼数。

新魔鬼数不能与上一位已有的魔鬼数重复,所以需要记录“开头有 \(2\)\(6\) 的非魔鬼数”。

为了得到这个,递推需要记录“开头有 \(1\)\(6\) 的非魔鬼数”和“开头有 \(0\)\(6\) 的非魔鬼数”。

那么状态转移方程便显而易见:

\(dp_{0,0}=0\)
\(dp_{i+1,j+1}+=dp_{i,j}\)
\(dp_{i+1,0}+=9 \times dp_{i,j}\)
\(dp_{i+1,3}+=10 \times dp_{i,3}\)

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=20;
const int N=5;

int t;
int n,m,k;
int dp[MAXN+5][N+5];

inline void init()
{
    dp[0][0]=1;
    for(register int i=0;i<=MAXN;i++)
    {
        for(register int j=0;j<3;j++)
        {
            dp[i+1][j+1]+=dp[i][j];
            dp[i+1][0]+=9*dp[i][j];
        }
        dp[i+1][3]+=10*dp[i][3];
    }
    return;
}

inline void solve()
{
    k=m=0;
    while(dp[m][3]<n)
        m++;
    for(register int i=m;i>=1;i--)
        for(register int j=0;j<=9;j++)
        {
            int cnt=dp[i-1][3];
            if(j==6 || k==3)
                for(register int l=max(3-k-(j==6),(long long)0);l<3;l++)
                    cnt+=dp[i-1][l];
            if(cnt<n)
                n-=cnt;
            else
            {
                if(k<3)
                    if(j==6)
                        k++;
                    else
                        k=0;
                printf("%lld",j);
                break;
            }
        }
    return;
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    init();
    cin>>t;
    while(t--)
    {
        cin>>n;
        solve();
        puts("");
    }
    return 0;
}

例四 P6218 Round Numbers S

\(dp_{i,j,k}\) 表示二进制 \(i\) 位数共有 \(j\)\(0\) 且最高位为 \(k\) 的圆数的数量。

方程:

\(dp_{i,j,0}+=dp_{i-1,j,1} + dp_{i-1,j-1,0}\)

\(dp_{i,j,1}+=dp_{i-1,j,1} + dp_{i-1,j,0}\)

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=50;
const int MOD=1e9+7;

int dp[MAXN][MAXN][MAXN];
int num[MAXN],cnt,sum[MAXN];

inline int dfs(int x,int tot,int tim,bool lim,bool flag)
{
    if(x==0)
    {
        if(tot<=tim)
            return 1;
        return 0;
    }
    if(!lim && !flag && dp[x][tot][tim]!=-1)
        return dp[x][tot][tim];
    int y=1;
    int ans=0;
    if(lim)
        y=num[x];
    for(register int i=0;i<=y;i++)
    {
        if(flag==1 && i==0)
            ans+=dfs(x-1,0,0,lim && i==y,1);
        else
            ans+=dfs(x-1,tot+(i==1),tim+(i==0),lim && i==y,0);
    }
    if(!lim && !flag)
        dp[x][tot][tim]=ans;
    return ans;
}

inline int solve(int x)
{
    memset(sum,0,sizeof(sum));
    cnt=0;
    while(x)
    {
        num[++cnt]=x&1ll;
        x>>=1ll;
    }
    return dfs(cnt,0,0,1,1);
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    memset(dp,-1,sizeof(dp));
    int l,r;
    cin>>l>>r;
    printf("%lld\n",solve(r)-solve(l-1));
    return 0;
}

九.斜率优化DP:

这个知识点的详解在第一道题里(包括板子)。

典型例题

例一 HDU3507 Print Article

分析:

首先想个朴素 DP:

\(dp_{i,j}\) 表示前 \(i\) 个元素分成 \(j\) 段的最小花费。

那么状态转移方程:

for(register int i=1;i<=n;i++)
    for(register int j=1;j<=i;j++)
        for(register int k=1;k<=i-1;k++)
            dp[i][j]=min(dp[i][j],dp[k][j-1]+m+(sum[i]-sum[k])*(sum[i]-sum[k]))

目测复杂度 \(O(n^3)\),过不了。

那么想些优化。

优化一:关于状态的优化。

\(dp_i\) 表示打印出前 \(i\) 个元素的最小花费。

那么状态转移方程:

for(register int i=1;i<=n;i++)
    for(register int j=1;j<=i;j++)
        dp[i]=min(dp[i],dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]));

这个时候我们将时间复杂度降到了 \(O(n^2)\)

优化二:斜率优化。

斜率优化 DP 状态转移方程的特征: \(dp_i=\min(dp_i,A\times dp_j+i\)\(j\) 的乘积项 \(+C)\)

实现步骤:

假设有两个决策点 \(k\)\(j\),且满足 \(k\lt j\)\(j\) 是更优解。

\[dp_j+m+(sum_i-sum_j)\times(sum_i-sum_j)\leqslant dp_k+m+(sum_i-sum_k)\times(sum_i-sum_k) \]

\[dp_j+{sum_i}^2-2\times sum_i\times sum_j+{sum_j}^2\leqslant dp_k+{sum_i}^2-2\times sum_i\times sum_k+{sum_k}^2 \]

\[dp_j+{sum_j}^2-dp_k-{sum_k}^2\leqslant 2\times sum_i\times(sum_j-sum_k) \]

\[\dfrac{dp_j+{sum_j}^2-dp_k-{sum_k}^2}{sum_j-sum_k}\leqslant 2\times sum_i \]

现在定义一个斜率为 \(\dfrac{Y_j-Y_k}{X_j-X_k}\)

所以

\[\dfrac{Y_j-Y_k}{X_j-X_k}\leqslant 2\times sum_i \]

只要满足这个条件,\(j\) 就是两者中的更优解,\(k\) 就可以润了。

那么这个时候我们就将时间复杂度降到了 \(O(n)\),这个时候就能够过掉这道题了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+5;

int dp[MAXN],q[MAXN],sum[MAXN];

inline int top(int j,int k)
{
    return dp[j]+sum[j]*sum[j]-(dp[k]+sum[k]*sum[k]);
}

inline int down(int j,int k)
{
    return sum[j]-sum[k];
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    int n,m;
    while(cin>>n>>m)
    {
        for(register int i=1;i<=n;i++)
            cin>>sum[i];
        sum[0]=dp[0]=0;
        for(register int i=1;i<=n;i++)
            sum[i]+=sum[i-1];
        int l=1,r=0;
        q[++r]=0;
        for(register int i=1;i<=n;i++)
        {
            while(l+1<=r && top(q[l+1],q[l])<=2*sum[i]*down(q[l+1],q[l]))
                l++;
            int j=q[l];
            dp[i]=dp[j]+m+(sum[i]-sum[j])*(sum[i]-sum[j]);
            while(l+1<=r && top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))
                r--;
            q[++r]=i;
        }
        printf("%lld\n",dp[n]);
    }
    return 0;
}

例二 P3195 玩具装箱

懒得推式子。。。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+5;

int n,m;
int a[MAXN],sum[MAXN],f[MAXN],g[MAXN];
int q[MAXN<<1],l,r;
int dp[MAXN];

inline double up(int j,int k)
{
    return 1.0*(dp[k]-dp[j]-g[j]+g[k])/(f[k]-f[j]);
}

inline int down(int i,int j)
{
    return dp[j]+(f[i]-f[j]-m-1)*(f[i]-f[j]-m-1);
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(register int i=1;i<=n;i++)
        cin>>a[i],sum[i]=sum[i-1]+a[i];
    for(register int i=1;i<=n;i++)
    {
        f[i]=sum[i]+i;
        g[i]=(f[i]+m+1)*(f[i]+m+1);
    }
    g[0]=(m+1)*(m+1);
    for(register int i=1;i<=n;i++)
    {
        while(l<r && up(q[l],q[l+1])<=2*f[i])
            l++;
        dp[i]=down(i,q[l]);
        while(l<r && up(q[r-1],q[r])>up(q[r],i))
            r--;
        q[++r]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}

例三 P3648 序列分割

首先写出一个朴素DP:

\(dp_{i,j}\) 表示打印出前 \(i\) 个元素,共切 \(j\) 次的最大分数,\(j\) 为上次切到的地方,\(k\) 为本次切的次数,\(sum_i\) 表示前 \(i\) 个元素的和。

\[dp_{i,k}=dp_{j,k-1}+sum_j \times (sum_i-sum_j)=dp_{j,k-1}+sum_i \times sum_j -{sum_j}^2 \]

然后来推一推式子。

当前有 \(j\)\(k\) 两个决策点,且 \(j\) 优于 \(k\),切了 \(w\) 次。

\[dp_{j,w-1}+sum_i \times sum_j -{sum_j}^2 > dp_{k,w-1}+sum_i \times sum_k -{sum_k}^2 \]

\[dp_{j,w-1}-dp_{k,w-1} + {sum_k}^2-{sum_j}^2 > -sum_i \times (sum_j-sum_k) \]

\[\dfrac{dp_{j,w-1}-dp_{k,w-1} + {sum_k}^2-{sum_j}^2}{sum_j-sum_k}> -sum_i \]

那么就出来了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5;

int n,k;
int sum[MAXN],a[MAXN];
int pre[MAXN][205];
int dp[MAXN][205];
int q[MAXN],l,r;

inline int top(int j,int k,int w)
{
    return dp[j][w-1]-sum[j]*sum[j]-dp[k][w-1]+sum[k]*sum[k];
}

inline int down(int j,int k)
{
    return sum[k]-sum[j];
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>k;
    for(register int i=1;i<=n;i++)
        cin>>a[i],sum[i]=sum[i-1]+a[i];
    for(register int w=1;w<=k;w++)
    {
        l=1,r=0;
        q[++r]=0;
        for(register int i=1;i<=n;i++)
        {
            while(l+1<=r && top(q[l+1],q[l],w)>=sum[i]*down(q[l+1],q[l]))
                l++;
            int j=q[l];
            dp[i][w]=dp[j][w-1]+sum[i]*sum[j]-sum[j]*sum[j];
            pre[i][w]=j;
            while(l+1<=r && top(i,q[r],w)*down(q[r],q[r-1])<=down(i,q[r])*top(q[r],q[r-1],w))
                r--;
            q[++r]=i;
        }
    }
    printf("%lld\n",dp[n][k]);
    int i=n;
    for(register int x=k;x>=1;x--)
    {
        i=pre[i][x];
        printf("%lld ",i);
    }
    return 0;
}

例四 P2900 Land Acquisition G

双倍经验:SP15086 ACQUIRE - Land Acquisition

首先来个朴素DP:

\(dp_i\) 表示分到第 \(i\) 块土地时的最小代价,\(j\) 为上个分到的田地,\(X_i\) 表示第 \(i\) 块土地的长,\(Y_i\) 表示第 \(i\) 块土地的宽。

\[dp_i=dp_j+X_i \times Y_{j+1} \]

照例推式子。

当前有两个决策点 \(j\)\(k\),且 \(j\)\(k\) 更优。

\[dp_j+X_i \times Y_{j+1} \leqslant dp_k+X_i \times Y_{k+1} \]

\[dp_j-dp_k\leqslant X_i \times (Y_{k+1}-Y_{j+1}) \]

\[\dfrac{dp_j-dp_k}{Y_{k+1}-Y_{j+1}}\leqslant X_i \]

\[\dfrac{dp_j-dp_k}{Y_{j+1}-Y_{k+1}}\geqslant -X_i \]

那么就出来了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e4+5;

int n;
int dp[MAXN];
int cnt;
int q[MAXN];

struct Node
{
    int w,l;
}node[MAXN];

inline bool cmp(Node a,Node b)
{
    if(a.w==b.w)
        return a.l>b.l;
    return a.w>b.w;
}

inline int top(int j,int k)
{
    return dp[j]-dp[k];
}

inline int down(int j,int k)
{
    return node[k+1].w-node[j+1].w;
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
        cin>>node[i].w>>node[i].l;
    sort(node+1,node+n+1,cmp);
    for(register int i=1;i<=n;i++)
        if(node[i].l>node[cnt].l)
            node[++cnt]=node[i];
    n=cnt;
    int l=1,r=0;
    q[++r]=0;
    for(register int i=1;i<=n;i++)
    {
        while(l+1<=r && top(q[l+1],q[l])<=node[i].l*down(q[l+1],q[l]))
            l++;
        int j=q[l]; 
        dp[i]=dp[j]+node[i].l*node[j+1].w;
        while(l+1<=r && top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))
            r--;
        q[++r]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}

例五 P2120 仓库建设

老套路,先写朴素DP:

\(dp_i\) 表示在 \(i\) 位置建立仓库的最小费用,\(j\) 表示上一次建立仓库的位置,\(sump_i=\sum\limits_{j=1}^ip_i\)\(sum_i=\sum\limits_{j=1}^ix_ip_i\)

\[dp_i=dp_j+x_i \times (sump_i-sump_j)-(sum_i-sum_j)+c_i \]

现在有两个决策点 \(j\)\(k\),且 \(j\) 优于 \(k\)

\[dp_j+x_i \times (sump_i-sump_j)-(sum_i-sum_j)<dp_k+x_i \times (sump_i-sump_k)-(sum_i-sum_k) \]

\[dp_j-dp_k+sum_j-sum_k<x_i\times(sump_j-sump_k) \]

\[\dfrac{(dp_j+sum_j)-(dp_k+sum_k)}{sump_j-sump_k}<x_i \]

那么就可以做了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e7+5;

int n;
int x[MAXN],p[MAXN],c[MAXN];
int sum[MAXN],sump[MAXN];
int q[MAXN];
int dp[MAXN];

inline int top(int j,int k)
{
    return (dp[j]+sum[j])-(dp[k]+sum[k]);
}

inline int down(int j,int k)
{
    return sump[j]-sump[k];
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    for(register int i=1;i<=n;i++)
    {
        cin>>x[i]>>p[i]>>c[i];
        sum[i]=sum[i-1]+p[i]*x[i];
        sump[i]=sump[i-1]+p[i];
    }
    int l=1,r=0;
    q[++r]=0;
    for(register int i=1;i<=n;i++)
    {
        while(l+1<=r && top(q[l+1],q[l])<=x[i]*down(q[l+1],q[l]))
            l++;
        int j=q[l];
        dp[i]=dp[j]+x[i]*(sump[i]-sump[j])-(sum[i]-sum[j])+c[i];
        while(l+1<=r && top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))
            r--;
        q[++r]=i;
    }
    int minn=0x7f7f7f7f7f7f7f7f;
    for(register int i=n;i>=1;i--)
    {
        minn=min(minn,dp[i]);
        if(p[i])
            break;
    }
    printf("%lld\n",minn);
    return 0;
}

例六 P3628 特别行动队

朴素DP:

\(dp_i\) 表示将前 \(i\) 个士兵分好组后的最大战力,\(j\) 表示上一次分组的位置,\(sum_i=\sum\limits_{j=1}^ix_j\)

\[dp_i=dp_j+a\times(sum_i-sum_j)^2+b\times(sum_i-sum_j)+c \]

\[dp_i=dp_j+a\times({sum_i}^2+{sum_j}^2-2\times sum_i\times sum_j)+b\times(sum_i-sum_j)+c \]

当前有 \(j\)\(k\) 两个决策点,且 \(j\) 优于 \(k\)

\[dp_j+a\times({sum_i}^2+{sum_j}^2-2\times sum_i\times sum_j)+b\times(sum_i-sum_j)\geqslant dp_k+a\times({sum_i}^2+{sum_k}^2-2\times sum_i\times sum_k)+b\times(sum_i-sum_k) \]

\[dp_j+a\times {sum_j}^2-2a\times sum_i\times sum_j-b\times sum_j\geqslant dp_k+a\times {sum_k}^2-2a\times sum_i\times sum_k-b\times sum_k \]

\[dp_j+a\times{sum_j}^2-b\times sum_j-(dp_k+a\times{sum_k}^2-b\times sum_k)\geqslant 2a\times sum_i\times (sum_j-sum_k) \]

\[\dfrac{dp_j+a\times{sum_j}^2-b\times sum_j-(dp_k+a\times{sum_k}^2-b\times sum_k)}{2a\times(sum_j-sum_k)}\leqslant sum_i \]

那么就可以做了。

Code

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e6+5;

int n;
int a,b,c;
int x[MAXN];
int q[MAXN];
int sum[MAXN];
int dp[MAXN];

inline int top(int j,int k)
{
    return dp[j]+a*sum[j]*sum[j]-b*sum[j]-(dp[k]+a*sum[k]*sum[k]-b*sum[k]);
}

inline int down(int j,int k)
{
    return 2*a*(sum[j]-sum[k]);
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n;
    cin>>a>>b>>c;
    for(register int i=1;i<=n;i++)
        cin>>x[i],sum[i]=sum[i-1]+x[i];
    int l=1,r=0;
    q[++r]=0;
    for(register int i=1;i<=n;i++)
    {  
        while(l+1<=r && top(q[l+1],q[l])>=down(q[l+1],q[l])*sum[i])
            l++;
        int j=q[l];
        dp[i]=dp[j]+a*(sum[i]-sum[j])*(sum[i]-sum[j])+b*(sum[i]-sum[j])+c;
        while(l+1<=r && top(i,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i,q[r]))
            r--;
        q[++r]=i;
    }
    printf("%lld\n",dp[n]);
    return 0;
}

例七 P5017 摆渡车

朴素DP:

\(dp_i\) 表示 \(i\) 时刻时等车的时间的最小值,\(j\) 为上个车出发的时刻,

\[dp_i=dp_j\sum\limits_{k=j+1}^i i-\sum\limits_{k=j+1}^it_k \]

\(cnt_i\) 表示 \(i\) 时刻前有多少人已到达,\(sum_i\) 表示 \(t_i\) 的前缀和,

\[dp_i=dp_j+(cnt_i-cnt_j) \times i-(sum_i-sum_j) \]

当前有两个决策点 \(j\)\(k\),且 \(j\) 优于 \(k\)

\[dp_j+(cnt_i-cnt_j) \times i-(sum_i-sum_j)\leqslant dp_k+(cnt_i-cnt_k) \times i-(sum_i-sum_k) \]

\[dp_j+sum_j-(dp_k+sum_k)\leqslant (cnt_j-cnt_k)\times i \]

\[\dfrac{dp_j+sum_j-(dp_k+sum_k)}{cnt_j-cnt_k}\leqslant i \]

Code

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=5000005;

int n,m;
int t[MAXN];
int dp[MAXN],sum[MAXN],cnt[MAXN];
int q[MAXN];

inline int top(int j,int k)
{
    return dp[j]+sum[j]-(dp[k]+sum[k]);
}

inline int down(int j,int k)
{
    return cnt[j]-cnt[k];
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(register int i=1;i<=n;i++)
        cin>>t[i];
    sort(t+1,t+n+1);
    for(register int i=1;i<=n;i++)
    {
        sum[t[i]]+=t[i];
        cnt[t[i]]++;
    }
    for(register int i=1;i<t[n]+m;i++)
    {
        sum[i]+=sum[i-1];
        cnt[i]+=cnt[i-1];
    }
    int l=1,r=0;
    q[++r]=0;
    for(register int i=0;i<t[n]+m;i++)
    {
        while(l+1<=r && top(i-m,q[r])*down(q[r],q[r-1])<=top(q[r],q[r-1])*down(i-m,q[r]))
            r--;
        q[++r]=i-m;
        while(l+1<=r && top(q[l+1],q[l])<=i*down(q[l+1],q[l]))
            l++;
        int j=q[l];
        dp[i]=dp[j]+(cnt[i]-cnt[j])*i-(sum[i]-sum[j]);
    }
    int minn=0x7f7f7f7f7f7f7f7f;
    for(register int i=t[n];i<t[n]+m;i++)
        minn=min(minn,dp[i]);
    printf("%d\n",minn);
    return 0;
}

十.四边形不等式:

1.用法:

对于形如以下的状态转移方程:

\[dp_{i,j}=\min\begin{Bmatrix}dp_{i,j},dp_{i,k-1}\times dp_{k,j}+cost_{k,j}\end{Bmatrix} \]

设辅助状态 \(pos_{i,j}\) 表示 \(dp_{i,j}\) 取最优值的决策点。

\(cost_{i,j}\) 满足以下两个条件:

1. 对于 \(l<l'<r'<r\)

\[cost_{l',r}+cost_{l,r'}\leqslant cost_{l,r}+cost_{l',r'} \]

但一般我们写成:

\[cost_{l+1,r+1}+cost_{l,r}\leqslant cost_{l+1,r}+cost_{l,r+1} \]

2. 对于 \(l<l'<r'<r\)

\[cost_{l',r'}\leqslant cost_{l,r},区间包含单调性 \]

上述两个条件成立,那么可以推导出 \(pos_{i,j}\) 具有以下单调性:

\[pos_{l,r-1}\leqslant pos_{l,r} \leqslant pos_{l+1,r} \]

或者

\[pos_{l,r}\leqslant pos_{l,r+1}\leqslant pos_{l+1,r+1} \]

决策点利用 \(pos\) 数组转移,可以将时间复杂度由 \(O(n^3)\) 降到 \(O(n^2)\)

时间复杂度证明:

\[pos_{l,r-1}\leqslant pos_{l,r} \leqslant pos_{l+1,r} \]

\(l=1\) 时,总共走 \(pos_{2,len}-pos_{1,len-1}\) 个;

\(l=2\) 时,总共走 \(pos_{3,len+1}-pos_{2,len}\) 个;

\(l=3\) 时,总共走 \(pos_{4,len+2}-pos_{3,len+1}\) 个;

......

\(l=n-len+1\) 时,总共走 \(pos_{n-len+2,n}-pos_{n-len+1,n-1}\) 个;

那么累加起来,总共走 \(pos_{n-len+2,n}-pos_{1,len-1}\) 个,

易知,

\[pos_{n-len+2,n}-pos_{1,len-1}< n \]

那么就只有 \(i\)\(j\) 的那两层循环,时间复杂度就是 \(O(n^2)\)

典型例题

例一 HDU3480 Division

朴素DP:

\(dp_{i,j}\) 表示前 \(j\) 个数分成 \(i\) 组的最小平方和,\(k\) 为上次分到的数,

\[dp_{i,j}=\min\begin{Bmatrix}dp_{i,j},dp_{i-1,k}+(a_j-a_{k+1})^2\end{Bmatrix} \]

我们先考虑斜率优化:

当前有两个决策点 \(p\)\(k\),且 \(p\) 优于 \(k\)

\[dp_{i-1,p}+(a_j-a_{p+1})^2\leqslant dp_{i-1,k}+(a_j-a_{k+1})^2 \]

\[dp_{i-1,p}-2\times a_j \times a_{p+1}+{a_{p+1}}^2\leqslant dp_{i-1,k}-2\times a_j\times a_{k+1}+{a_{k+1}}^2; \]

\[dp_{i-1,p}-dp_{i-1,k}+{a_{p+1}}^2-{a_{k+1}}^2\leqslant 2\times a_j\times (a_{p+1}-a_{k+1}) \]

\[\dfrac{dp_{i-1,p}-dp_{i-1,k}+{a_{p+1}}^2-{a_{k+1}}^2}{a_{p+1}-a_{k+1}}\leqslant 2\times a_j \]

然后这题的四边形不等式做法很简单。

四边形不等式:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e4+5;
const int MAXM=5e3+5;

int t,n,m;
int a[MAXN];
int dp[MAXM][MAXN];
int pos[MAXM][MAXN];

inline void solve()
{
    for(register int i=1;i<=n;i++)
        cin>>a[i];
    sort(a+1,a+n+1);
    memset(dp,0x3f,sizeof(dp));
    for(register int i=1;i<=n;i++)
        pos[0][i]=1;
    dp[0][0]=0;
    for(register int i=1;i<=m;i++)
    {
        dp[i][i]=0;
        pos[i][i]=i;
        pos[i][n+1]=n;
        for(register int j=n;j>i;j--)
        {
            for(register int k=pos[i-1][j];k<=pos[i][j+1];k++)
                if(dp[i][j]>dp[i-1][k-1]+(a[j]-a[k])*(a[j]-a[k]))
                {
                    dp[i][j]=dp[i-1][k-1]+(a[j]-a[k])*(a[j]-a[k]);
                    pos[i][j]=k;
                }
        }
    }
    return;
}

int main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    cin>>t;
    for(register int i=1;i<=t;i++)
    {
        cin>>n>>m;
        solve();
        printf("Case %d: %d\n",i,dp[m][n]);
    }
    return 0;
}

例二 HDU2829 Lawrence

首先我们要考虑如何处理两两之间乘积的和,我们可以用一个前缀和来记录一下,并拆一下就可以得到,其他都是板子,详见代码。

点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=1e3+5;

int dp[MAXN][MAXN];
int pos[MAXN][MAXN];
int sum[MAXN],prod[MAXN];
int a[MAXN];
int n,m;

inline void DP()
{
    memset(dp,0x3f,sizeof(dp));
    for(register int i=1;i<=n;i++)
    {
        cin>>a[i];
        sum[i]=sum[i-1]+a[i];
        prod[i]=prod[i-1]+a[i]*a[i];
        pos[i][0]=1;
    }
    dp[0][0]=0;
    for(register int i=1;i<=m+1;i++)
    {
        dp[i][i]=0;
        pos[n+1][i]=n;
        pos[i][i]=i;
        for(register int j=n;j>=i;j--)
            for(register int k=pos[j][i-1];k<=pos[j+1][i];k++)
            {
                if(dp[j][i]>dp[k-1][i-1]+((sum[j]-sum[k-1])*(sum[j]-sum[k-1])-(prod[j]-prod[k-1]))/2)
                {
                    dp[j][i]=dp[k-1][i-1]+((sum[j]-sum[k-1])*(sum[j]-sum[k-1])-(prod[j]-prod[k-1]))/2;
                    pos[j][i]=k;
                }
            }
    }
    printf("%lld\n",dp[n][m+1]);
    return;
}

signed main()
{
    ios_base::sync_with_stdio(false);
    cin.tie(0),cout.tie(0);
    while(cin>>n>>m)
    {
        if(n==0 && m==0)
            break;
        DP();
    }
    return 0;
}

posted @ 2022-05-04 17:04  Code_AC  阅读(79)  评论(1编辑  收藏  举报