动态规划

Posted iwillenter-top1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了动态规划相关的知识,希望对你有一定的参考价值。

(1.String) (Painter,Chengdu) (2008,LA) (4394)

题意:

给定两个长度相等,只有小写字母组成的字符串(s)(t),每步可以把(s)的一个连续子串“刷”称同一个字母,问至少需要多少不才能把(s)变成(t)。比如,(s=bbbbbbb)(t=aaabccb),最少需要两步可实现将(s)变成(t)(bbbbbbb->aaabbbb->aaabccb)

分析:

一个套路的区间(DP),和许多区间(DP)相似的是这里要设两个状态,一个是(f[i])表示把(s)的前(i)个字符变成和(t)一样所需要的最小步数,一个是(dp[i][j])表示的是把一个空串的([i,j])区间变得和(t)一样所需要的最少步数。

贪心出一个性质,即每次刷一个串,都可以从头开始刷起,如果第一个字符相同,则从第二个开始刷,以此类推,而每次刷要么只刷一个字符,要么刷到另一个与目标串首字符相同的字符为止。

对于(dp)数组的转移,所以我们肯定只有当(t[i]==t[j])的时候我们的粉刷才会是优秀的。

而对于(f)数组,我们当(s[i]=t[i])的时候直接继承上一个状态即可,而当不同时类似(dp)数组的转移即可。

(Code:)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int maxn=110;

char s[maxn],t[maxn];

int dp[maxn][maxn],f[maxn];

int main()
{
    cin>>(s+1)>>(t+1);
    int l=strlen(s+1);
    for(int i=l;i>=1;--i)
    {
        dp[i][i]=1;
        for(int j=i+1;j<=l;++j)
        {
            if(t[i]==t[j]) dp[i][j]=dp[i+1][j];
            else dp[i][j]=dp[i+1][j]+1;
            for(int k=i+1;k<=j;++k)
            {
                if(t[k]==t[i]) dp[i][j]=min(dp[i][j],dp[i+1][k]+dp[k+1][j]);//因为
                //t[k]=t[i],所以dp[i+1][k]=dp[i][k]
            }
        }
    }
    for(int i=1;i<=l;++i)
    {
        if(s[i]==t[i]) f[i]=f[i-1];
        else
        {
            f[i]=maxn;
            for(int j=i;j;--j)
            {
                if(t[i]==t[j]) f[i]=min(f[i],f[j-1]+dp[j][i]);
            }
        }
    }
    printf("%d
",f[n]);
    return 0;
}

(2.Parade,Bejing) (2008,LA) (4327)

题意:

(F)城由(n+1)个横向路和(m+1)个竖向路组成。你的任务是从最南边的路走到最北边的路,使得走过的路上的高兴值和最大(高兴值可能为负值)。同一段路不能经过两次,且不能从北往南走,在每条横向路所花时间不超过(k)

数据范围:

(0leq kleq 3000000,1leq nleq 100,leq mleq 10000)

分析:

我们设(d[i][j])表示他走到((i,j))的时候的最大的开心值(只有横向道路才有开心值),所以我们可以得到:
[ d[i][j]=max(d[i][j],d[i+1][p]+sum[i][j]-sum[i][p]),L[i][j]leq pleq j-1\d[i][j]=max(d[i][j],d[i+1][p]+sum[i][p]-sum[i][j]),j+1leq pleq R[i][j] ]
注意,各段道路的可行走时间(k)是不一样的,所以我们用(L[i][j])来表示能走到((i,j))这个位置的上一行的最左边的位置,类似的,我们的(R[i][j])就用来表示能走到((i,j))这个位置的最右边的位置。(sum[i][j])都表示(i)(j)列的开心值之和。

很明显我们要用一个双端队列来维护这样的满足条件的点(p),也即是单调队列(注意我们从北往南把行编号为(1)~(n))。

还要注意一下,因为我们有(m+1)条竖向道路,那么也就代表着一行有(m+1)个点,那么一行就有(m)条横向道路。

总的来说就是一个单调队列动态规划而已。

而我们下面的代码中是直接把从南向北用从北向南代替(也就是说此时就不能从南向北走了,这个条件实际上只是保证我们不会往回走并没有太多的实际意义)。

(Code:)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<queue>
#define ll long long
using namespace std;

const int maxn=110;

deque<int> dq;

int val[maxn][maxn*maxn];

int sum[maxn][maxn*maxn];

int t[maxn][maxn*maxn];

int dis[maxn][maxn*maxn];

int n,m,k;

int d[maxn][maxn*maxn];

int ans;

template<class T>void read(T &x)
{
    bool f=0;char ch=getchar();x=0;
    for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
    if(f) x=-x;
}

int main()
{
    read(n);read(m);read(k);
    for(int i=1;i<=n+1;++i)
    {
        for(int j=1;j<=m;++j)
        {
            read(val[i][j]);
            sum[i][j]=sum[i][j-1]+val[i][j];
        }
    }
    for(int i=1;i<=n+1;++i)
    {
        for(int j=1;j<=m;++j)
        {
            read(t[i][j]);
            dis[i][j]=dis[i][j-1]+t[i][j];
        }
    }
    for(int i=1;i<=n+1;++i)
    {
        dq.clear();
        dq.push_front(0);//因为单调队列中的元素i,它们已经在上一行中被走过了,所以在算的时候是不
        //需要给它们减一的
        for(int j=0;j<=m;++j)
        {
            while(!dq.empty()&&dis[i][j]-dis[i][dq.front()]>k)
                dq.pop_front();
            d[i][j]=d[i-1][j];
            if(!dq.empty())
                d[i][j]=max(d[i][j],d[i-1][dq.front()]+sum[i][j]-sum[i][dq.front()]);
            while(!dq.empty()&&d[i][j]>=d[i][dq.back()]+sum[i][j]-
                  sum[i][dq.back()])
                dq.pop_back();
            dq.push_back(j);
        }
        dq.clear();
        dq.push_front(m);
        for(int j=m;j>=0;--j)
        {
            while(!dq.empty()&&dis[i][dq.front()]-dis[i][j]>k) dq.pop_front();
            if(!dq.empty())
                d[i][j]=max(d[i][j],d[i-1][dq.front()]+sum[i][dq.front()]-sum[i][j]);
            while(!dq.empty()&&d[i][j]>=d[i][dq.back()]+sum[i][dq.back()]-
                  sum[i][dq.back()])
                dq.pop_back();
            dq.push_back(j);
        }
    }
    for(int i=1;i<=m;++i) ans=max(ans,d[n+1][i]);
    printf("%d
",ans);
    return 0;
}

(3.Cave,Chengdu) (2007,LA) (4015)

题意:

一颗(n)各节点的有根树,树的边有正整数权,表示两个节点之间的距离。你的任务是回答这样的询问:从根节点出发,走不超过(x)单位距离,最多能经过多少个节点?同一个节点经过多次只算一个。

数据范围:

节点数(1le nle 500),边权(1le dle 10000),询问个数(1le Qle 1000,)(0le xle 5000000)

分析:

树形(DP)(树形背包)。

我们设(d[i][j][0])表示从(i)出发在它的子树中访问了(j)个节点之后返回到(i)的最短距离,同理设(d[i][j][1])表示从(i)出发在它的子树中访问了(j)个节点之后不用返回到(i)的最短距离,那么我们就可以得出状态转移方程:
[ d[i][j][0]=min{d[i][j-k][0]+d[son_i][k][0]}(0le k< sz[son_i])d[i][j][1]=minegin{cases}d[i][j-k][0]+d[son_i][k][1]+2 imes dis[i][son_i],\d[i][j-k][1]+d[son_i][k][0]+2 imes dis[i][son_i])end{cases}(0le k< sz[son_i]) ]

(Code:)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;

const int maxn=510;
const int inf=0x7fffffff;

ll d[maxn][maxn][2];

int sz[maxn];

int Q,n;

ll x;

int head[maxn],tot;
struct Edge
{
    int to,nxt,w;
    Edge(){};
    Edge(int to,int nxt,int w):to(to),nxt(nxt),w(w){};
}ed[maxn<<1];
void add(int u,int v,int w)
{
    ed[++tot]=Edge(v,head[u],w);
    head[u]=tot;
    ed[++tot]=Edge(u,head[v],w);
    head[v]=tot;
}

template<class T>void read(T &x)
{
    bool f=0;char ch=getchar();x=0;
    for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
    if(f) x=-x;
}

void init(int u,int fa)
{
    sz[u]=1;
    for(int i=head[u];i;i=ed[i].nxt)
    {
        int v=ed[i].to;
        if(v==fa) continue;
        init(v,u);
        sz[u]+=sz[v];
    }
}

void dfs(int u,int fa)
{
    for(int i=0;i<=sz[u];++i) d[u][i][0]=d[u][i][1]=inf;
    d[u][1][0]=d[u][1][1]=0;
    for(int i=head[u];i;i=ed[i].nxt)
    {
        int v=ed[i].to;
        if(v==fa) continue;
        dfs(v,u);
        for(int j=sz[u];j>1;--j)
        {
            for(int k=min(sz[v],j-1);k;--k)
            {
                d[u][j][0]=min(d[u][j][0],d[u][j-k][0]+d[v][k][0]);
                d[u][j][1]=min(d[u][j][1],d[u][j-k][0]+d[v][k][1]+2*ed[i].w);
                d[u][j][1]=min(d[u][j][1],d[u][j-k][1]+d[v][k][0]+2*ed[i].w);
            }
        }
    }
}

int main()
{
    read(n);
    for(int i=1;i<=n-1;++i)
    {
        int u,v,w;
        read(u);read(v);read(w);
        add(u,v,w);
    }
    read(Q);
    init(1,0);
    dfs(1,0);
    while(Q--)
    {
        read(x);
        int Max=0;
        for(int i=1;i<=n;++i) if(min(d[1][i][0],d[1][i][1])<=x) Max=max(Max,i);
        printf("%d
",Max);
    }
    return 0;
}

(4.Help) (Bubu,Wuhan) (2009,LA) (4490)

题意:

书架上有(n)本书。如果从左到右写下书架上每本书的高度,我们能够得到一个序列,比如(30,30,31,31,32)。我们把相邻的高度相同的书看成一个片段,并且定义该书架的混乱程度为片段的个数。比如,(30,30,31,31,32)的混乱程度为(3)。同理,(30,32,32,31)的混乱程度也是(3),但(31,32,31,32,31)的混乱程度高达(5)(请想象一下这个书架,确实够乱的吧)。

为了整理书架,你最多可以拿出(k)本书,然后再把它们插回书架(其他书的相对顺序保持不变),是书架的混乱程度降至最低)。

数据范围:

(1leq kle nleq 100),高度(1le h_ileq 8)

分析:

看到这么小的数据范围就应该想到状态压缩,这道题的正解是状压(DP)

有一篇题解中提到了一句话,我觉得挺实用的,区间(DP)一般是整体考虑,而一般的(DP)一般是考虑以(i)为末尾。

我们设(d[i][j][S][l])表示现在考虑到了第(i)本书,已经取了(k)本书出来,在我们在之前仍留在书架的书的高度集合为(S),而书架上的最后一本书的高度为(l)的最小混乱程度。

我们的初始化就应该为:(d[i+1][i][2^{h[i+1]}][h[i+1]]=1)

然后我们考虑当前的这本书取不取:

若要取,则:(d[i+1][j+1][k][l]=min(d[i+1][j+1][k][l],d[i][j][k][l]))

若不取,则:(d[i+1][j+1][k|2^{h[i+1]}][l]=min(d[i+1][j+1][k|2^{h[i+1]}][l],d[i][j][k][l]+(h[i+1]!=l)))

而我们设计(S)这一维的功能就体现在了答案的统计这一块上:

(ans=min(ans,d[i][j][k][l]+count(k)))(其中的(count(i))表示(i)的二进制位下有多少个(1))。

(Code:)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

int d[110][110][1<<9][9];

int n,k;

int d[110];

int two[8];

template<class T>void read(T &x)
{
    bool f=0;char ch=getchar();x=0;
    for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
    if(f) x=-x;
}

int main()
{
    read(n);read(k);
    two[0]=1;
    int S=0;
    for(int i=1;i<=8;++i) two[i]=two[i-1]*2;
    for(int i=1;i<=n;++i) read(h[i]),S|=two[h[i]];
    memset(d,0x3f,sizeof(d));
    int ans=d[0][0][0][0];
    d[1][0][two[h[1]]][h[1]]=1;
    for(int i=1;i<=n;++i)
    {
        d[i+1][i][two[h[i+1]]][h[i+1]]=1;
        for(int j=0;j<=k&&j<=i;++j)
        {
            for(int s=0;s<two[8];++s)
            {
                for(int l=0;l<8;++l)
                {
                    if(d[i][j][s][l]!=ans)
                    {
                        d[i+1][j+1][s][l]=min([i][j][s][l],d[i+1][j+1][s][l]);
                        d[i+1][j][s|two[h[i+1]]][h[i+1]]=
                            min(d[i+1][j][s|two[h[i+1]]][h[i+1]],
                                d[i][j][s][l]+(h[i+1]!=l));
                    }
                }
            }
        }
    }
    for(int i=1;i<=n;++i)
    {
        for(int j=0;j<=min(i,k);++j)
        {
            for(int k=0;k<two[8];++k)
            {
                for(int l=0;l<8;++l)
                {
                    ans=min(ans,d[i][j][k][l]+builtin_pop_count(S^k));//加上了那些被拿下
                    //来的书的混乱值
                }
            }
        }
    }
    printf("%d
",ans);
    return 0;
}

(5.Masud) (Rana,UVa) (11600)

题意:

某国有(n)个城市,编号为(1-n)。这些城市两两之间都有一条双向道路(一共有(frac{n(n-1)}{2})条),其中一些路上有妖怪,其他路是安全的。为了保证城市间两两可达,你第一天晚上住在城市(1),然后每天白天随机选择一个新的城市,然后顺着它与当前所在城市之间的道路走过去,途中消灭这条道路上所有的妖怪,晚上住在这座城市。在平均情况下,需要多少个白天才能让任意两个城市之间均可以不经过有妖怪的道路而相互可达。

数据范围:

(1leq nleq 30,0le mle nleqfrac{n(n-1)}{2})

分析:

这是一个概率(DP)问题。

我们首先把那些两两之间可以不经过有妖怪的路而互相可达的连通分量找出来,那么从一个连通分量走到另一个连通分量一定是要经过那些有妖怪的路的,那么我们看到(n)的个数还蛮小的,所以我们可以考虑使用状态压缩。

我们设(d[S])表示遍历了的联通分量的状态为(S)的期望天数,那么下一步我们的转移有两种,一种是走到了一个已经变了的联通分量中的点,一种是走到了一个之前都还没有遍历过的联通分量中的一个点,那么写出状态转移方程就是:
[ d[S]=sum_{iin S} PR_i imes d[S]+1+sum_{iin UxorS}(PR_{i} imes d[S|belong[i]]) ]
移项一下得到:
[ (1-frac{sz[S]-1}{n-1}) imes d[S]=1+sum_{iin(UxorS)}(frac{n-sz[S]}{n-1} imes d[S|belong[i]])\frac{n-sz[S]}{n-1} imes d[S]=1+frac{1}{n-1} imessum d[S|new_S]\d[S]=frac{n-1}{n-sz[S]}+frac{1}{n-sz[S]} imessum d[S|new_S]\d[S]=frac{n-1}{n-sz[S]} imes(frac{1}{n-1} imes sum d[S|new_S]+1) ]
然后就可以写出代码了。

很明显这是开不下的(但事实上数据比较水,用二进制是可以存下的),所以我们就使用(map)来存。

(Code:)

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#include<vector>
#include<map>
using namespace std;

const int maxn=30;

int n,m;

map<int,double> d;
map<int,double>::iterator it;

int state[maxn];

int fa[maxn];
int find(int x)
{
    if(x!=fa[x]) fa[x]=find(fa[x]);
    return fa[x];
}
void unionn(int x,int y)
{
    int fx=find(x),fy=find(y);
    if(fx!=fy)
    {
        fa[fx]=fy;
        state[fy]|=state[fx];
    }
}

template<class T>void read(T &x)
{
    bool f=0;char ch=getchar();x=0;
    for(;ch<'0'||ch>'9';ch=getchar()) if(ch=='-') f=1;
    for(;ch>='0'&&ch<='9';ch=getchar()) x=x*10+ch-'0';
    if(f) x=-x;
}

double trans;

double dp(int S)
{
    if((it=d.find(S))!=d.end()) return it->second;
    double ans=0;
    int sz=0;
    for(int i=0;i<n;++i)
    {
        if(S&(1<<i))
        {
            ++sz;
            continue;
        }
        ans+=dp(S|state[i]);
    }
    ans=(n-1.)/(n-sz)*(trans*ans+1);
    d.insert(make_pair(S,ans));
    return ans;
}

void init()
{
    for(int i=0;i<n;++i) state[i]=(1<<i);
}

int kase;

int main()
{
    int T;
    read(T);
    while(T--)
    {
        read(n);read(m);
        if(n==1)
        {
            printf("Case %d: 0",++kase);
            continue;
        }
        for(int i=1;i<=n;++i) fa[i]=i;
        init();
        for(int i=1;i<=m;++i)
        {
            int x,y;
            read(x);read(y);
            --x,--y;
            unionn(x,y);
        }
        for(int i=0;i<n;++i)
        {
            state[i]=state[find(i)];
        }
        trans=1./(n-1);
        d.clear();
        d.insert(make_pair((1<<n)-1,0));
        printf("Case %d: %lf
",dp(state[0]));
    }
    return 0;
}

以上是关于动态规划的主要内容,如果未能解决你的问题,请参考以下文章

是否可以动态编译和执行 C# 代码片段?

动态规划_线性动态规划,区间动态规划

应对笔试手写代码,如何准备动态规划?

应对笔试手写代码,如何准备动态规划?

应对笔试手写代码,如何准备动态规划?

算法动态规划 ⑤ ( LeetCode 63.不同路径 II | 问题分析 | 动态规划算法设计 | 代码示例 )