动态规划

Posted iwillenter-top1

tags:

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

补充(一)中的部分

以下所有计算中都是将一个字符串当做(s[1]-s[n])

(LCS)(最长公共子序列)

我们设(d[i][j])表示第一个串的前(i)位与第二个串的前(j)位的最长公共子序列的长度。

(a[i]==b[j])的时候,(d[i][j]=d[i-1][j-1]+1)

(a[i]!=b[j])的时候,(d[i][j]=max(d[i-1][j],d[i][j-1]))

(LCS)(最长公共子串)

我们设(d[i][j])表示匹配到第一个串的第(i)位和第二个串的第(j)位的公共子串的长度。(即此时的这个子串一定是(i)(j)都包含在其中的)

(a[i]==b[j])的时候,(d[i][j]=d[i-1][j-1]+1)

(a[i]!=b[j])的时候,(d[i][j]=0)

(LPS)(最长回文子序列)

其实就是(UVa11404)的问题

我们设(d[i][j])表示从(i)(j)的最长回文子序列的长度。

(s[i]==s[j])的时候,(d[i][j]=d[i+1][j-1]+2)

(s[i]!=s[j])的时候,(d[i][j]=max(d[i+1][j],d[i][j-1]))

(Manacher)求最长回文子串

回文半径数组(r[])用来记录以每个字符为回文中心求出的回文半径长度(包括它本身)。

另外由于回文串分为奇回文和偶回文,在处理奇偶的时候较为繁琐,所以我们在这里运用一个小技巧,在字符串首尾以及个字符之间插入一个未在字符串中出现过的字符。

故可以发现(r[i]-1)(因为插入了其他的字符且一直延伸到了插入的字符处)即是以(i)为回文中心的回文串的长度。

我们设置两个变量(mx)(id)(mx)表示的是最大回文子串的右边界(也就是说最长回文子串无法到达(mx)这个位置),而(id)则表示最长回文子串的中心位置。即(mx=id+r[id])

之后分情况讨论,实在是无力解释了,直接放代码吧,代码上会有一些适当的注释。

Code:(Manacher)

string Manacher(string s)
{
    string res="$#";//在最开头的地方还要多插入一个字符,避免越界的麻烦
    for(int i=0;i<s.size();++i)//先改造字符串
    {
        res+=s[i];
        res+="#";
    }
    int id=0,mx=0;//id记录的是能到达的最右边的回文串的中心点,mx记录最长回文子序列最右端不能到达
    //的第一个点
    int maxlen=0,maxpoint=0;//maxlen记录最长回文子序列的长度,maxpoint记录最长回文子序列的中
    //心点
    
    for(int i=0;i<res.size();++i)
    {
        if(mx>i) r[i]=min(r[2*id-i],mx-i);//如果i在原来的最长回文子序列内
        else r[i]=1;
        while(res[i-r[i]]==res[i+r[i]]) ++r[i];
        if(i+r[i]>mx)
        {
            mx=i+r[i];
            id=i;
        }
        if(r[i]-1>maxlen) maxlen=r[i]-1,maxpoint=i;
    }
    
    if(maxlen&1) return s.substr(i-(maxlen-1)/2,maxlen);//这里可以自己手推一下
    else return s.substr(i-maxlen/2+1,maxlen);
}

1.Cellular Network,Seoul 2009,LA 4731

题意:

手机在蜂窝网络中的定位是一个重要问题。假设蜂窝网络已经得知手机处于(c_1,c_2,cdots ,c_n)这些区域中的一个,最简单的方法是同事在这些区域中寻找手机。但这样做很浪费宽带。由于蜂窝网络中可以得知手机在不同区域中的概率,因此一个折中的方法就是把这些区域分成(w)组,然后依次访问。比如,已知手机可能位于5个区域中,概率分别为(0.3、0.05、0.1、0.3)(0.25)(w=2),则一种方法是先同时访问(left[c_1,c_2,c_3 ight]),在同时访问(left[c_4,c_5 ight]),访问区域数的数学期望为(3*(0.3+0.05+0.1)+(3+2)*(0.3+0.25)=4.1)。另一种方法是先同时访问(left[c_1,c_4 ight]),再访问(left[c_2,c_3,c_5 ight]),访问区域数的数学期望为(2*(0.3+0.3)+(3+2)*(0.05+0.1+0.25)=3.2)

求期望的最小值。

分析:

一开始读题连题意都读不懂(可能是已经有些困倦了的缘故),但是在网上看第二篇题解的时候忽然发现这道题真的好简单。我就不该对首尔的信竞题抱有什么很难的期望......

我们设状态状态(d[i][j])表示前(i)个数分成(j)组的期望值,我们首先就可以从样例中得出把较大者放在前面更优,所以我们就可以得出状态转移方程:
[ d[i][j]=min(d[i][j],d[k][j-1]+i*(sum[i]-sum[k-1])) ]

Code:

/*
    Problem ID:LA 4731
    Author:dolires
    Date: 28/09/2019 21:44
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

const int maxn=1e4+10;
const int inf=0x7fffffff;

double d[110][110];

int u[maxn];

double p[maxn];
double sum[maxn];

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()
{
    int n,w;
    read(n);read(w);
    int All=0;
    for(int i=1;i<=n;++i) read(u[i]),All+=u[i];
    for(int i=1;i<=n;++i) p[i]=u[i]/All;
    sort(p+1,p+n+1);
    for(int i=1;i<=n;++i) sum[i]=sum[i-1]+p[i],d[i][1]=i*sum[i];
    for(int i=1;i<=n;++i)
    {
        for(int j=2;j<=w;++j) d[i][j]=inf;
    }
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=w;++j)
        {
            for(int k=1;k<i;++k)
            {
                d[i][j]=min(d[i][j],d[k][j-1]+i*(sum[i]-sum[k-1]));
            }
        }
    }
    printf("%.1lf
",d[n][w]);
    return 0;
}

2.Mega Man‘s Missions,UVa 11795

题意:

洛克人最初只有一个武器“Mega Buster”。你需要按照一定的顺序消灭(n)个其他机器人。每消灭一个机器人将会得到他的武器,而某些机器人只能用特定的武器才能消灭。你的任务是计算出可以消灭所有机器人的顺序总数。

(n<=16)

分析:

看到题面的时候依然毫无头绪。看到数据范围(n<=16)时,想到应该是状态压缩动态规划,果然就是。

而且其实好像很简单(每次都只知道在看了题解之后说这句话),而且做了这道题之后有点悲伤,因为我感觉这种状态压缩入门题,在座的所有人中就只有我不能切掉它了。

我们设(d[s])表示kill掉的机器人的集合为(s)的时候的方案数。

但是它并不仅仅是一个简单的(其实就是)状压DP,因为它对于消灭机器人的种类数有前提限制,所以我们要首先预处理出从每个点可以到达的点(即每个机器人可以消灭的机器人),然后进行状压DP即可。(非常类似于曾经做过的一道题集合计算机,简直是一模一样,但是我再做这道题居然还是做不起,再让我悲伤落泪1分钟)

Code:

/*
    Problem ID:UVa 11795
    Author:dolires
    Date: 28/09/2019 22:32
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
#define ll long long
using namespace std;

const int maxn=20;

ll d[1<<maxn];
int re[maxn];

int p[1<<maxn];

char s[maxn];

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()
{
    int T;
    read(T);
    while(T--)
    {
        int n;
        read(n);
        for(int i=0;i<n;++i)
        {
            cin>>s;
            for(int j=0;j<n;++j)
            {
                if(s[j]=='1')
                {
                    re[i]|=(1<<j);
                }
            }
        }
        int S=(1<<n)-1;
        for(int i=S;i;(i-1)&S)
        {
            for(int j=0;j<n;++j)
            {
                if(i&(1<<j))
                {
                    p[i]|=re[j];
                }
            }
        }
        d[0]=1;
        for(int i=0;i<=S;++i)
        {
            if(!d[i]) continue;//因为之后枚举的状态要由d[i]来更新
            for(int j=0;j<n;++j)
            {
                if(p[i]&(1<<j)!=0&&(i&(1<<j))==0)
                {
                    d[i|(1<<j)]+=d[i];
                }
            }
        }
    }
}

3.Jump,Seoul 2009,LA 4727

题意:

(1-n)按逆时针顺序排成一个圆圈,从(1)开始每(k)个数字删掉一个,直到所有数字都被删除。这些数的删除顺序记为(Jump(n,k))(n,k>=1))。

例如,(Jump(10,2)=[2,4,6,8,10,3,7,1,9,5])(Jump(13,3)=[3,6,9,12,2,7,11,4,10,5,1,8,13])(Jump(13,10)=[10,7,5,4,6,9,13,8,3,12,1,11,2])(Jump(10,9)=[9,10,3,8,1,6,4,5,7,2])

你的任务是求出(Jump(n,k))的最后3个数。

分析:

看到题的第一想法是模拟,觉得有可能时间复杂度会爆,第二想法是找规律(反正怎么样都和动态规划搭不上边),再一看,啊,首尔的ACM题(应该是的吧),再想想之前也有一道约瑟夫问题的变形,应该是递推。

天,好像这作为一道ACM题也过于简单了吧。

因为每次删除数后都会给每个数重新编号,并把作为起点开始的数的编号记为(0)(在最后加一即可),所以我们不难得出,最后一个留下的数在最后的编号应该为(0),然后我们再一步一步把它的编号还原回去,也就是:
[ dp[i]=(dp[i-1]+k)\% iquad dp[1]=0 ]
接着再考虑倒数二个被留下的数的编号,再游戏的倒数第二轮,只剩下它和最后一个被留下的数,从(0)开始往后数第(k)个数即是被删除的数,所以我们可以得出倒数第二个被留下的数也即倒数第二个被删除的数的编号应该为((0+k-1)\%2)(因为(0)即是第一个被数到的数),也即是((k+1)\%2)(运用了同余的性质(-1equiv1(modquad2))。

同理,倒数第三个被留下的数的编号就应该是((0+k-1)\%3=(k+2)%3)(同样的(-1equiv2(modquad3))

有人可能会疑惑,那为什么在还原的时候我们就不减一呢?

原因是我们最后剩下的那个数的编号在最后的时候编号为(0),也就是说它是上一轮中第(k+1)个被数到的数,也就是说从上一轮中的(0)开始,((0+k)\%2)也就是它在上一轮中的编号。以此类推即可。

Code:

/*
    Problem ID:LA 4727
    Author: dolires
    Date: 29/09/2019 18:52
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<cmath>
using namespace std;

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()
{
    int T;
    read(T);
    while(T--)
    {
        int n,k;
        read(n);read(k);
        int x=(k+2)%3;
        for(int i=4;i<=n;++i)//从倒数第四轮开始向后递推
        {
            x=(x+k)%i;
        }
        printf("%d
",x+1);
        x=(k+1)%2;
        for(int i=3;i<=n;++i)
        {
            x=(x+k)%i;
        }
        printf("%d
",x+1);
        x=0;
        for(int i=2;i<=n;++i)
        {
            x=(x+k)%i;
        }
        printf("%d
",x+1);//因为编号实际上是从1开始的,所以每次输出的时候都要加上一
    }
    return 0;
}

总之,能理解就理解,不能够理解,你背都要背住类似于约瑟夫问题的结论:

若已知它在在某一轮中的编号为(f(i)),那么它在上一轮中的编号应该为(f(i+1)=(f(i)+k)\%(i+1))

4.Martian Mining,LA 3530

题意:

给出(n*m)网格中每个格子的(A)矿和(B)矿的数量,(A)矿必须由左向右运输,(B)矿必须由下向上运输,如图(1-60)所示。管子不能拐弯或者间断。要求收集到的(A、B)矿总量尽量大。

技术图片

分析:

本题是棋盘类的DP(我一直不太懂什么是棋盘类的DP,今天正好趁这个机会好好学习一下)。

我们设状态为(d[i][j]),表示走到((i,j))这个点时,能得到的最优解,那么很容易(对于我这种菜鸡来讲当然不容易)得出状态转移方程:
[ d[i][j]=max(d[i-1][j]+suma[i][j],d[i][j-1]+sumb[i][j]) ]
如果它在((i,j))这个点选择运(A)矿,,因为管子不能拐弯或者间断,那么第(i)行前(j)列都只能选择运(A)矿。

解释一下这个状态转移方程,因为当它在((i,j))这个点选择选择运(A)矿时,那它上面是(A)矿还是(B)矿还是没矿并不重要,只要取能获得价值最大的即可,而此处的价值就是它上面最大的价值以及第(i)行前(j)列中(A)矿的价值总和,若选择运(B)矿也是同理。

Code:

/*
    Problem ID:LA 3530,UVa 1366
    Author: dolires
    Date: 29/09/2019 20:49
*/
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;

const int maxn=510;

int suma[maxn][maxn],sumb[maxn][maxn];
int n,m;
int numa[maxn][maxn],numb[maxn][maxn];
int d[maxn][maxn];

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);
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            read(numa[i][j]);read(numb[i][j]);
        }
    }
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            suma[i][j]=suma[i][j-1]+numa[i][j];
            sumb[i][j]=sumb[i-1][j]+numb[i][j];
        }
    }
    int Max=0;
    for(int i=1;i<=n;++i)
    {
        for(int j=1;j<=m;++j)
        {
            d[i][j]=max(d[i-1][j]+suma[i][j],d[i][j-1]+sumb[i][j]);
            Max=max(Max,d[i][j]);
        }
    }
    printf("%d
",Max);
    return 0;
}

又由于篇幅原因,本篇不得不提前结束了,若有需要的朋友,请移步动态规划(三)继续观看。

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

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

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

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

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

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

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