[NOIP复习]第三章:动态规划

一、背包问题

最基础的一类动规问题,相似之处在于给n个物品或无穷多物品或不同种类的物品,每种物品只有一个或若干个,给一个背包装入这些物品,要求在不超出背包容量的范围内,使得获得的价值或占用体积尽可能大,这一类题的动规方程f[i]一般表示剩余容量为i时取得的最大价值或最大占用体积,或者有多维状态,分别表示不同种物品的剩余量

1、Wikioi 1014 装箱问题

有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每个物品有一个体积(正整数)。

要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。

一个整数v,表示箱子容量

一个整数n,表示有n个物品

接下来n个整数,分别表示这个物品的各自体积

一个整数,表示箱子剩余空间。

24

6

8

3

12

7

9

7

0

一道经典的背包动规,用数组f[]进行动规,f[v]=剩余容量为v时可以利用的最大体积,那么可以在每次输入一个物品体积cost时遍历剩余容量状态,当前状态的剩余容量为v时,可以选择装入物品(装入物品则当前状态可以利用的体积为f[v-cost]+cost)或不装入物品,推出动规方程:f[v]=max{f[v-cost]+cost}

#include <stdio.h>
#include <string.h>

#define MAXN 30000

int f[MAXN]; //f[i]=剩余体积为i时装入物品的最大体积

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	int v,n,cost;
	scanf("%d%d",&v,&n);
	for(int i=1;i<=n;i++) 
	{
		scanf("%d",&cost);
		for(int j=v;j>=cost;j--)
			f[j]=max(f[j],f[j-cost]+cost);
	}
	printf("%d\n",v-f[v]);
	return 0;
}

 

2、Wikioi 1068 乌龟棋

题目描述 Description

小明过生日的时候,爸爸送给他一副乌龟棋当作礼物。 乌龟棋的棋盘是一行N个格子,每个格子上一个分数(非负整数)。棋盘第1格是唯一 的起点,第N格是终点,游戏要求玩家控制一个乌龟棋子从起点出发走到终点。

…… 1 2 3 4 5 ……N 乌龟棋中M张爬行卡片,分成4种不同的类型(M张卡片中不一定包含所有4种类型 的卡片,见样例),每种类型的卡片上分别标有1、2、3、4四个数字之一,表示使用这种卡 片后,乌龟棋子将向前爬行相应的格子数。游戏中,玩家每次需要从所有的爬行卡片中选择 一张之前没有使用过的爬行卡片,控制乌龟棋子前进相应的格子数,每张卡片只能使用一次。 游戏中,乌龟棋子自动获得起点格子的分数,并且在后续的爬行中每到达一个格子,就得到 该格子相应的分数。玩家最终游戏得分就是乌龟棋子从起点到终点过程中到过的所有格子的 分数总和。 很明显,用不同的爬行卡片使用顺序会使得最终游戏的得分不同,小明想要找到一种卡 片使用顺序使得最终游戏得分最多。 现在,告诉你棋盘上每个格子的分数和所有的爬行卡片,你能告诉小明,他最多能得到 多少分吗?

输入描述 Input Description

输入的每行中两个数之间用一个空格隔开。 第1行2个正整数N和M,分别表示棋盘格子数和爬行卡片数。 第2行N个非负整数,a1a2……aN

,其中ai表示棋盘第i个格子上的分数。 第3行M个整数,b1b2……bM

,表示M张爬行卡片上的数字。 输入数据保证到达终点时刚好用光M张爬行卡片,即N - 1=∑(1->M) bi

输出描述 Output Description

输出一行一个整数

样例输入 Sample Input

13 8

4 96 10 64 55 13 94 53 5 24 89 8 30

1 1 1 1 1 2 4 1

样例输出 Sample Output

455

数据范围及提示 Data Size & Hint

【数据范围】

对于30%的数据有1 ≤ N≤ 30,1 ≤M≤ 12。

对于50%的数据有1 ≤ N≤ 120,1 ≤M≤ 50,且4 种爬行卡片,每种卡片的张数不会超

过20。

对于100%的数据有1 ≤ N≤ 350,1 ≤M≤ 120,且4 种爬行卡片,每种卡片的张数不会

超过40;0 ≤ ai ≤ 100,1 ≤ i ≤ N;1 ≤ bi ≤ 4,1 ≤ i ≤M。输入数据保证N−1=ΣM

i b

1

可以说这是一道背包的变形题,不再是只有单一的状态(剩余体积),实际上因为卡片分种类,导致状态变成了4个:4种爬行卡片分别剩余的张数,则可用数组f[][][][]来进行动规,f[i][j][k][h]=1、2、3、4号卡片各用掉i,j,k,h张时,下棋获得的最大分数,则每次从小到大动规,经过状态f[i][j][k][h]时,考虑用掉一张1或2或3或4号卡片,那么这次操作之前得到的分数就是f[i-1][k][j][h]或f[i][k-1][j][h]或f[i][k][j-1][h]或f[i][k][j][h-1](注:上次操作得到的分数并不包含上次操作后到达的格子分数),然后再加上上次操作后到达的格子分数,这次操作到达的格子分数就等到下次操作时再算。最后输出f[card[1]][card[2]][card[3]][card[4]]+map[n],card[x]=第x种卡片的张数,map[n]=终点的分数。

当然也可以先算上起点的格子分数,每次决策时不加上次操作到达的格子分数,而是加上本次操作到达的格子分数,或许更便于理解

#include <stdio.h>
#include <string.h>

#define MAXM 40
#define MAXN 400

int f[MAXM][MAXM][MAXM][MAXM]; //f[i][j][k][h]=1 2 3 4四种卡片各用了i,j,k,h张时的最高分数
int map[MAXN]; //棋盘上的分数

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	int n,m;
	int card[5]={0};
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)
		scanf("%d",&map[i]);
	for(int i=0;i<m;i++)
	{
		int kind;
		scanf("%d",&kind);
		card[kind]++;
	}
	for(int i=0;i<=card[1];i++)
		for(int j=0;j<=card[2];j++)
			for(int k=0;k<=card[3];k++)
				for(int h=0;h<=card[4];h++)
				{
					int dis=i*1+j*2+k*3+h*4; //dis=当前用了i,j,k,h张牌后乌龟棋移动的距离
					if(i>=1) f[i][j][k][h]=max(f[i][j][k][h],f[i-1][j][k][h]+map[1+(i-1)*1+j*2+k*3+h*4]);
					if(j>=1) f[i][j][k][h]=max(f[i][j][k][h],f[i][j-1][k][h]+map[1+i*1+(j-1)*2+k*3+h*4]);
					if(k>=1) f[i][j][k][h]=max(f[i][j][k][h],f[i][j][k-1][h]+map[1+i*1+j*2+(k-1)*3+h*4]);
					if(h>=1) f[i][j][k][h]=max(f[i][j][k][h],f[i][j][k][h-1]+map[1+i*1+j*2+k*3+(h-1)*4]);
				}
	printf("%d\n",f[card[1]][card[2]][card[3]][card[4]]+map[n]);
	return 0;
}

3、POJ 1276 Cash Machine(多重背包经典题)

http://poj.org/problem?id=1276

题目大意:要用n种钱币凑面额cash,要取凑到的面额必须小于等于Cash,给定每种钱币的面值和张数,求最多能凑到的面额。

经典的多重背包题,可以将每种钱币当成物品,钱币的体积和价值均为它的面值。

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 11
#define MAXV 100010

using namespace std;

struct Thing
{
    int n,v,w; //n件,价值为v,体积为w,在这个题中v=w
}things[MAXN];//保存每种纸币个数

bool canGet[MAXV]; //canGet[i]=true表示面额i可以被取到

int main()
{
    int cash,n;
    while(scanf("%d%d",&cash,&n)!=EOF)
    {
        int i,j,ans=0; //要取的面额为cash,n种钱币
        for(int i=1;i<=n;i++)
        {
            scanf("%d%d",&things[i].n,&things[i].v);
            things[i].w=things[i].v;
        }
        if(!cash||!n)
        {
            printf("0\n");
            continue;
        }
        memset(canGet,false,sizeof(canGet));
        canGet[0]=true; //面额为0当然可以取到
        for(i=1,ans=0;i<=n;i++) //正在取第i种钱币
            for(j=ans;j>=0;j--) //取到的面额为j
                if(canGet[j]) //面额j可以取到
                    for(int k=1;k<=things[i].n;k++) //第i种物品取n个
                    {
                        int temp=j+k*things[i].w;
                        if(temp>cash) //取的面额超过了cash,太多了
                            break;
                        canGet[temp]=true;
                        if(temp>ans) ans=temp;
                    }
        cout<<ans<<endl;
    }
    return 0;
}

4、POJ 1742 Coins(带优化的多重背包)

http://poj.org/problem?id=1742

楼教主的经典题,题目大意是要用多种钱币凑出一个面额,这个面额小于等于v,给出每种钱币的面值和个数,求最终能凑出多少种面额

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <algorithm>

#define MAXN 110
#define MAXV 100100

using namespace std;

struct Thing
{
    int n,v,w; //个数为n,价值为v,体积为w
}coins[MAXN];

bool canGet[MAXV]; //canGet[i]=true表示面额i可以被取到
int n,v;

void ZeroOnePack(int cost) //01背包
{
    for(int i=v;i>=cost;i--)
        canGet[i]|=canGet[i-cost];
}

void CompletePack(int cost) //完全背包
{
    for(int i=cost;i<=v;i++)
        canGet[i]|=canGet[i-cost];
}

void MultiplePack(int cost,int amount) //多重背包
{
    if(cost*amount>=v)
    {
        CompletePack(cost); //转换为完全背包问题
        return;
    }
    int k=1; //拿k件同样的物品
    while(k<amount)
    {
        ZeroOnePack(k*cost);
        amount-=k;
        k<<=1; //k<-k*2
    }
    ZeroOnePack(amount*cost);
}

int main()
{
    while(scanf("%d%d",&n,&v))
    {
        int ans=0;
        if(!n||!v) break;
        for(int i=1;i<=n;i++) scanf("%d",&coins[i].w);
        for(int i=1;i<=n;i++) scanf("%d",&coins[i].n);
        memset(canGet,false,sizeof(canGet));
        canGet[0]=true;
        for(int i=1;i<=n;i++) //当前正在尝试凑的硬币为第i个硬币
            if(coins[i].n) //这个硬币有剩余
                MultiplePack(coins[i].w,coins[i].n);
        for(int i=1;i<=v;i++) //面额为i
            if(canGet[i]) //面额为i的可以取到
                ans++;
        cout<<ans<<endl;
    }
    return 0;
}




二、区间型动态规划

这一类题目的相似之处在于动规方程f[i]表示以i结尾的价值/数量等等的大小,在决策时寻找i之前的位置j,使得f[i]取得最值

3、Wikioi 1044 拦截导弹

    某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。

  

输入导弹依次飞来的高度(雷达给出的高度数据是不大于30000的正整数)

  

输出这套系统最多能拦截多少导弹,如果要拦截所有导弹最少要配备多少套这种导弹拦截系统。

389 207 155 300 299 170 158 65 

6

2

导弹的高度<=30000,导弹个数<=20

[NOIP复习]第三章:动态规划_第1张图片

可以把导弹的高度化为一个数字序列,由题意可知,一个导弹拦截的目标必为原序列的不上升子序列,要想让拦截目标个数尽量多,就要求这个不上升子序列是最长的,换句话说,第一问就是求不上升子序列,第二问略微麻烦点,要让所有目标都被打而且导弹尽量少,那么需要每个导弹不光打一个目标,而且要把以这个目标为头的不上升子序列都打到,如图,绿色的线就是不上升子序列,红色的线是严格上升子序列,很明显,第二问要求最长严格上升子序列,这个子序列中的所有元素都需要一发导弹,而它们连接的不上升子序列都能被这些导弹打到,第二问的答案就是最长严格上升子序列。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 1000

int up[MAXN],dn[MAXN];
int high[MAXN],cnt=0;
int maxDN=-1,maxUP=-1;

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	while(scanf("%d",&high[++cnt])!=EOF);
	for(int i=1;i<=cnt;i++)
		for(int j=1;j<i;j++)
		{
			if(high[i]>high[j])
			{
				up[i]=max(up[i],up[j]+1);
			}
			if(high[i]<=high[j])
			{
				dn[i]=max(dn[i],dn[j]+1);
			}
		}
	for(int i=1;i<=cnt;i++)
	{
		maxDN=max(maxDN,dn[i]);
		maxUP=max(maxUP,up[i]);
	}
	printf("%d\n%d\n",maxDN,maxUP+1);
	return 0;
}

4、Wikioi 3027 线段覆盖2

数轴上有n条线段,线段的两端都是整数坐标,坐标范围在0~1000000,每条线段有一个价值,请从n条线段中挑出若干条线段,使得这些线段两两不覆盖(端点可以重合)且线段价值之和最大。

n<=1000

第一行一个整数n,表示有多少条线段。

接下来n行每行三个整数, ai bi ci,分别代表第i条线段的左端点ai,右端点bi(保证左端点<右端点)和价值ci。

输出能够获得的最大价值

3

1 2 1

2 3 2

1 3 4

4

数据范围

对于40%的数据,n10

对于100%的数据,n1000

0<=ai,bi<=1000000

0<=ci<=1000000

首先需要把所有线段根据右端点升序排序,这样的话,找与第i条线段不重合的线段j,就只需要往前找一个线段j,使得j的右端点小于等于i的左端点坐标,然后开一个数组f[]进行动规,数组f[i]=前i条线段,第i条线段必选,获得的最大价值,那么对于每一条线段i,在这条线段之前找一条不和它重合的线段j,使得f[j]+value[i]取得最大值,DP方程为:f[i]=max{f[j]+value[i]},j<i且线段i不与j重合 

 

#include <stdio.h>
#include <string.h>
#include <algorithm>

#define MAXN 1100

using namespace std;

struct Line
{
	int L,R,w; //左端点,右端点,线段权值
}line[MAXN];

bool cmp(Line a,Line b)
{
	return a.R<b.R;
}

int f[MAXN]; //f[i]=前i条线段,第i条线段必选,获得的最大价值

int main()
{
	int n,ans=-1;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		scanf("%d%d%d",&line[i].L,&line[i].R,&line[i].w);
	sort(line+1,line+n+1,cmp);
	for(int i=1;i<=n;i++) f[i]=line[i].w;
	for(int i=2;i<=n;i++)
	{
		for(int j=1;j<i;j++) //找到第i条线段之前的一条线段j,两条线段互不重合,这样就能把i插到j的后面
			if(line[i].L>=line[j].R)
			{
				f[i]=max(f[i],line[i].w+f[j]);
			}
	}
	for(int i=1;i<=n;i++)
		ans=max(ans,f[i]); //找到了一个结尾i,使得f[i]最大
	printf("%d\n",ans);
	return 0;
}

三、区间型动态规划

这一类题的解法通常是开动规数组f[][](一般是两维),f[i][j]表示区间[i,j]获得的最值,决策时寻找一个属于该区间的中点k,使f[i][k]+f[k+1][j]取得最值,并用来更新f[i][j]

5、Wikioi 石子合并

有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。问安排怎样的合并顺序,能够使得总合并代价达到最小。

第一行一个整数n(n<=100)

第二行n个整数w1,w2...wn  (wi <= 100)

一个整数表示最小合并代价

4

4 1 1 4

18

这个题很明显是一个区间型动规,用f[i][j]表示将区间[i,j]合并时的最小代价,则每次决策时寻找一个中点k,使得f[i][k]+f[k+1][j]最小,并更新f[i][j]

另外这个题需要注意一下,i是从大到小枚举的,我个人理解是:i从大到小枚举,最开始动规决策时需要的已求出的f就很少,否则刚开始决策时很多需要的f没有求出来,答案就会错误 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 1000
#define INF 10000000

int f[MAXN][MAXN]; //f[i][j]=合并区间[i,j]所获得的最大价值
int sum[MAXN];

int min(int a,int b)
{
	if(a<b) return a;
	return b;
}

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		int w;
		scanf("%d",&w);
		sum[i]=sum[i-1]+w;
	}
	for(int i=n-1;i>=1;i--)
		for(int j=i+1;j<=n;j++)
		{
			int minAns=INF;
			for(int k=i;k<j;k++)
			{
				minAns=min(minAns,f[i][k]+f[k+1][j]+sum[j]-sum[i-1]);
			}
			f[i][j]=minAns;
		}
	printf("%d\n",f[1][n]);
	return 0;
}

6、Wikioi 1154 能量项链

Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用,这两颗珠子才能聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n(Mars单位),新产生的珠子的头标记为m,尾标记为n

需要时,Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上只剩下一颗珠子为止。显然,不同的聚合顺序得到的总能量是不同的,请你设计一个聚合顺序,使一串项链释放出的总能量最大。

例如:设N=44颗珠子的头标记与尾标记依次为(23) (35) (510) (102)。我们用记号⊕表示两颗珠子的聚合操作,(jk)表示第jk两颗珠子聚合后所释放的能量。则第41两颗珠子聚合后释放的能量为:

(41)=10*2*3=60

这一串项链可以得到最优值的一个聚合顺序所释放的总能量为

((41)2)3=10*2*3+10*3*5+10*5*10=710

第一行是一个正整数N4N100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1iN),当i<N< span>时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。

至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。

只有一行,是一个正整数EE2.1*109),为一个最优聚合顺序所释放的总能量。

4

2 3 5 10

710

这个题同样是区间型动态规划,和上一题很相似,最大的不同在于这个题的区间是一个环形的(没有起点和终点),通常处理这种环形区间的问题,可以把有n个元素的环看作2*n长度的区间,[n+1,2*n]从[1,n]复制而来,这样就不存在区间终点该怎么和起点连接的问题,然后动规时需要先枚举一个合并区间的起点i,那么合并区间就是[i,i+n-1],然后环形区间就转化成了一个直线区间,动规过程类似于上一题,时间复杂度O(n^4)

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 1000

struct node
{
	int head,tail;
}ball[MAXN];

int f[MAXN][MAXN]; //f[i][j]=将[i,j]合并后获得的最大能量

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int getEnergy(int m,int r,int n)
{
	return ball[m].head*ball[r].tail*ball[n].tail;
}

int main()
{
	int n;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&ball[i].head);
		ball[i-1].tail=ball[i].head;
	}
	ball[n].tail=ball[1].head;
	for(int i=1;i<=n;i++)
		ball[i+n]=ball[i];
	for(int i=1;i<=n;i++) //start from position i,[i,i+n-1]
		for(int j=i+n-2;j>=i;j--)
		{
			for(int k=j+1;k<=i+n-1;k++)
			{
				int maxAns=-1;
				for(int p=j;p<k;p++)
					maxAns=max(maxAns,f[j][p]+f[p+1][k]+getEnergy(j,p,k));
				f[j][k]=maxAns;
			}
		}
	int maxAns=-1;
	for(int i=1;i<=n;i++)
		maxAns=max(maxAns,f[i][i+n-1]);
	printf("%d\n",maxAns);
	return 0;
}

四、棋盘型动态规划

个人认为这种动规是最简单的,因为它非常形象,很好推导出动规方程,这一类动规大多开二维动规数组,代表棋盘里每个坐标的状态

7、Wikioi 1010 过河卒
 

 如图,A 点有一个过河卒,需要走到目标 B 点。卒行走规则:可以向下、或者向右。同时在棋盘上的任一点有一个对方的马(如上图的C点),该马所在的点和所有跳跃一步可达的点称为对方马的控制点。例如上图 C 点上的马可以控制 9 个点(图中的P1,P2 … P8 和 C)。卒不能通过对方马的控制点。


  棋盘用坐标表示,A 点(0,0)、B 点(n,m)(n,m 为不超过 20 的整数,并由键盘输入),同样马的位置坐标是需要给出的(约定: C不等于A,同时C不等于B)。现在要求你计算出卒从 A 点能够到达 B 点的路径的条数。

1<=n,m<=15

 键盘输入
   B点的坐标(n,m)以及对方马的坐标(X,Y){不用判错}

  屏幕输出
    一个整数(路径的条数)。

 6 6 3 2

17

如描述

这个题比较好做,只要在棋盘中标明所有马的控制点,动规时避开它们即可,f[i][j]表示经过(i,j)的路径条数,这个路径条数实际上就等于其左边的点和上方的点的路径条数之和,因为那两个点的路可以走到这个点,DP方程f[i][j]=f[i-1][j]+f[i][j-1],边界条件f[0][0]=1(左上角的点只可能有一条路经过) 

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 100

int map[MAXN][MAXN];
int f[MAXN][MAXN];
int n,m;

bool inMap(int x,int y)
{
	if(x<0||x>n||y<0||y>m) return false;
	return true;
}

int main()
{
	int x,y;
	scanf("%d%d%d%d",&n,&m,&x,&y);
	if(inMap(x,y)) map[x][y]=1;
	if(inMap(x+2,y+1)) map[x+2][y+1]=1;
	if(inMap(x+2,y-1)) map[x+2][y-1]=1;
	if(inMap(x-2,y+1)) map[x-2][y+1]=1;
	if(inMap(x-2,y-1)) map[x-2][y-1]=1;
	if(inMap(x+1,y+2)) map[x+1][y+2]=1;
	if(inMap(x+1,y-2)) map[x+1][y-2]=1;
	if(inMap(x-1,y+2)) map[x-1][y+2]=1;
	if(inMap(x-1,y-2)) map[x-1][y-2]=1;
	f[0][0]=1;
	for(int i=0;i<=n;i++)
		for(int j=0;j<=m;j++)
			if(!map[i][j])
			{
				if(inMap(i-1,j)) f[i][j]+=f[i-1][j];
				if(inMap(i,j-1)) f[i][j]+=f[i][j-1];
			}
	printf("%d\n",f[n][m]);
	return 0;
}



 8、Wikioi 1169 传纸条

小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题。一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。纸条要经由许多同学传到对方手里,小渊坐在矩阵的左上角,坐标(1,1),小轩坐在矩阵的右下角,坐标(m,n)。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。

在活动进行中,小渊希望给小轩传递一张纸条,同时希望小轩给他回复。班里每个同学都可以帮他们传递,但只会帮他们一次,也就是说如果此人在小渊递给小轩纸条的时候帮忙,那么在小轩递给小渊的时候就不会再帮忙。反之亦然。

还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低(注意:小渊和小轩的好心程度没有定义,输入时用0表示),可以用一个0-100的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度只和最大。现在,请你帮助小渊和小轩找到这样的两条路径。

输入的第一行有2个用空格隔开的整数m和n,表示班里有m行n列(1<=m,n<=50)。

接下来的m行是一个m*n的矩阵,矩阵中第i行j列的整数表示坐在第i行j列的学生的好心程度。每行的n个整数之间用空格隔开。

输出共一行,包含一个整数,表示来回两条路上参与传递纸条的学生的好心程度之和的最大值。

3 3

0 3 9

2 8 5

5 7 0

34

30%的数据满足:1<=m,n<=10

100%的数据满足:1<=m,n<=50

这个题略有些特殊,如果纸条只从上往下传,但不往回传的话,动规思路很清晰:用f[i][j]表示到达(i,j)时的最大分数,则这个最大分数可以从左边的格子和上面的格子递推出来,f[i][j]=max(f[i-1][j],f[i][j-1])+map[i][j]

[NOIP复习]第三章:动态规划_第2张图片

如图,上面的图是单方向传纸条的过程,下面的图是双向传纸条的过程,很明显下面的过程实际上相当于两个纸条同时向下传,而且其路径不能重合,这就是双线动规,可以开一个四维数组表示当前两张纸条分别所处的坐标(状态),然后决策部分类似上面的DP方程。

而且双线动规很明显的特征就是数据范围比较小,因为双线动规无论是空间还是时间复杂度都很高,如本题棋盘大小不超过50*50

 

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 60

int f[MAXN][MAXN][MAXN][MAXN]; //f[i][j][k][h]=经过(i,j)和(k,h)时获得的最大分数
int map[MAXN][MAXN];

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	int n,m;
	scanf("%d%d",&m,&n);
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++)
			scanf("%d",&map[i][j]);
	for(int i=1;i<=m;i++)
		for(int j=1;j<=n;j++)
			for(int k=1;k<=m;k++)
				for(int h=1;h<=n;h++)
					if(i!=k&&j!=h)
					{
						f[i][j][k][h]=max(f[i][j][k][h],f[i-1][j][k-1][h]+map[i][j]+map[k][h]);
						f[i][j][k][h]=max(f[i][j][k][h],f[i][j-1][k-1][h]+map[i][j]+map[k][h]);
						f[i][j][k][h]=max(f[i][j][k][h],f[i-1][j][k][h-1]+map[i][j]+map[k][h]);
						f[i][j][k][h]=max(f[i][j][k][h],f[i][j-1][k][h-1]+map[i][j]+map[k][h]);
					}
	printf("%d\n",max(f[m-1][n][m][n-1],f[m][n-1][m-1][n]));
	return 0;
}


 9、骑士游历

设有一个n*m的棋盘(2≤n≤50,2m≤50),如下图,在棋盘上有一个中国象棋马。

规定:

1)马只能走日字

2)马只能向右跳

问给定起点x1,y1和终点x2,y2,求出马从x1,y1出发到x2,y2的合法路径条数。

第一行2个整数n和m

第二行4个整数x1,y1,x2,y2

输出方案数

30 30

1 15 3 15

2

2<=n,m<=50

同样是棋盘型动规,非常类似于过河卒,只不过不需要判断是否有障碍物,而是决策变复杂了一点,不过也很简单,不细说了,动规循环时,由于马只能往右走,但是往上往下走都行,所以横坐标循环从x1到x2就够了,但纵坐标循环要从1到m,否则会错

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 100

long long int f[MAXN][MAXN],map[MAXN][MAXN];
long long int n,m,x1,y1,x2,y2;

bool inMap(long long int x,long long int y)
{
	if(x<0||x>n||y<0||y>m) return false;
	return true;
}

int main()
{
	scanf("%lld%lld%lld%lld%lld%lld",&n,&m,&x1,&y1,&x2,&y2);
	f[x1][y1]=1;
	for(int i=x1;i<=x2;i++)
		for(int j=1;j<=m;j++)
		{
			if(inMap(i-1,j-2)) f[i][j]+=f[i-1][j-2];
			if(inMap(i-1,j+2)) f[i][j]+=f[i-1][j+2];
			if(inMap(i-2,j-1)) f[i][j]+=f[i-2][j-1];
			if(inMap(i-2,j+1)) f[i][j]+=f[i-2][j+1];
		}
	printf("%lld\n",f[x2][y2]);
	return 0;
}


五、划分型动态规划

这一类题共同之处在于对一个序列拆分成n个部分,动规方程通常为二维的,f[i][j]表示将前i个数划分j次获得的答案最值

10、Wikioi 1017 乘积最大 

 

今年是国际数学联盟确定的“2000——世界数学年”,又恰逢我国著名数学家华罗庚先生诞辰90周年。在华罗庚先生的家乡江苏金坛,组织了一场别开生面的数学智力竞赛的活动,你的一个好朋友XZ也有幸得以参加。活动中,主持人给所有参加活动的选手出了这样一道题目:

 

设有一个长度为N的数字串,要求选手使用K个乘号将它分成K+1个部分,找出一种分法,使得这K+1个部分的乘积能够为最大。

 

同时,为了帮助选手能够正确理解题意,主持人还举了如下的一个例子:

 

有一个数字串:312, 当N=3,K=1时会有以下两种分法:

 

1)  3*12=36

2)  31*2=62

  

   这时,符合题目要求的结果是:31*2=62

 

   现在,请你帮助你的好朋友XZ设计一个程序,求得正确的答案。

   程序的输入共有两行:

   第一行共有2个自然数N,K(6≤N≤40,1≤K≤6)

   第二行是一个长度为N的数字串。

   结果显示在屏幕上,相对于输入,应输出所求得的最大乘积(一个自然数)。

4  2

1231

62

本题由于比较老,数据实际也比较小,用long long 即可通过

此题就是一道划分型动态规划,可以用二维动规方程f[i][j]表示将前i个数用j个乘号划分所获得的最大值,那么我们可以在区间[1,j]中找一点k,让[1,k]用j-1个乘号划分,然后[k+1,j]不用乘号划分,第k个数之后有一个乘号,这样获得的结果为f[k][j-1]*num(k+1,j),num(k+1,j)表示区间[k+1,j]所代表的数,这个题不必多增加一维来表示区间的左端点,因为动规过程是从左到右动规,乘号从少到多动规,而且答案的左端点就是1,所以没有必要,另外有的题解中右端点枚举避开了左区间中的乘号和没有用完的乘号,我直接枚举[1,n]也没有错误,也不会超时

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 1000

long long int f[MAXN][MAXN]; //f[i][j]=前j个数用i个乘号划分获得的最大值
char s[MAXN]; //输入的数字串
int num[MAXN]; //输入的数字串

long long int getNum(int L,int R) //将数字串中的区间[L,R]转化成数字
{
	long long int ans=0;
	for(int i=L;i<=R;i++)
	{
		ans*=10;
		ans+=num[i];
	}
	return ans;
}

long long int max(long long int a,long long int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	long long int n,k;
	scanf("%lld%lld%s",&n,&k,s+1);
	for(int i=1;i<=n;i++)
		num[i]=s[i]-'0';
	for(int i=1;i<=n;i++) f[i][0]=getNum(1,i);
	for(int i=1;i<=k;i++) //用i个乘号划分区间[1,i]
		for(int j=1;j<=n;j++)
		{
			for(int mid=1;mid<j;mid++) //分成左区间[1,mid],右区间[mid+1,j]
				f[j][i]=max(f[j][i],f[mid][i-1]*getNum(mid+1,j));
		}
	printf("%lld\n",f[n][k]);
	system("pause");
	return 0;
}

 11、Wikioi 1039 数的划分

将整数n分成k份,且每份不能为空,任意两种划分方案不能相同(不考虑顺序)。
例如:n=7,k=3,下面三种划分方案被认为是相同的。
1 1 5

1 5 1

5 1 1
问有多少种不同的分法。

输入:n,k (6<n<=200,2<=k<=6)


输出:一个整数,即不同的分法。

 7 3

4

 {四种分法为:1,1,5;1,2,4;1,3,3;2,2,3;}

同样是划分型动态规划,不过这题需要一点数学技巧,用f[i][j]表示将数字i划分为j个部分的方案,那么有两种划法:1、每个部分数字都不为1,方案数就等于i-j划分为j个部分的方案数,方案数为f[i-j][j];2、有一个部分数字为1,方案数就等于i-1划分为j-1个部分的方案数,f[i-1][j-1],所以DP方程为f[i][j]=f[i-j][j]+f[i-1][j-1],另外DP边界为f[i][i]=1,f[i][1]=1,i属于[1,n],f[0][0]=1

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define MAXN 1000

int f[MAXN][MAXN]; //f[n][k]=divide number n into k parts,the answer is f[n][k]

int max(int a,int b)
{
	if(a>b) return a;
	return b;
}

int main()
{
	int n,k;
	scanf("%d%d",&n,&k);
	f[0][0]=1;
	for(int i=1;i<=n;i++)
	{
		f[i][1]=1;
		f[i][i]=1;
	}
	for(int i=1;i<=n;i++)
		for(int j=1;j<=k;j++)
			if(j<=i)
			{
				f[i][j]=f[i-1][j-1]+f[i-j][j];
			}
	printf("%d\n",f[n][k]);
	return 0;
}


六、状态压缩型动态规划 

1、Wikioi 2800 送外卖

有一个送外卖的,他手上有n份订单,他要把n份东西,分别送达n个不同的客户的手上。n个不同的客户分别在1~n个编号的城市中。送外卖的从0号城市出发,然后n个城市都要走一次(一个城市可以走多次),最后还要回到0点(他的单位),请问最短时间是多少。现在已知任意两个城市的直接通路的时间。

第一行一个正整数n (1<=n<=15)

接下来是一个(n+1)*(n+1)的矩阵,矩阵中的数均为不超过10000的正整数。矩阵的i行j列表示第i-1号城市和j-1号城市之间直接通路的时间。当然城市a到城市b的直接通路时间和城市b到城市a的直接通路时间不一定相同,也就是说道路都是单向的。

一个正整数表示最少花费的时间

3
0 1 10 10
1 0 1 2
10 1 0 10
10 2 10 0

8

1<=n<=15

这个题其实就是TSP问题,可以用二进制数来表示一个状态集合,1表示访问了该城市,0表示没有访问过该城市,将集合表示为如下图

[NOIP复习]第三章:动态规划_第3张图片

通过位运算来减少、增加集合中的元素,DP方程也比较好想,在访问过的城市集合S中找一个城市j,使得在城市j后再访问i,让总路程最短

f(S,i)=min{f(S-{i},j)}+dist(j,i),j属于S且j!=i

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

#define INF 1000000
#define MAXN 1000

int dist[MAXN][MAXN];
int f[1<<16][MAXN]; //f[status][start]=当前已经走过的城市集合为status,最近访问过的城市为start的最少行走距离
int n;

//f(S,i)=min{f(S-{j},i)+dist[i][j]}

int min(int a,int b)
{
	if(a<b) return a;
	return b;
}

void floyd()
{
	for(int k=0;k<=n;k++)
		for(int i=0;i<=n;i++)
			for(int j=0;j<=n;j++)
				dist[i][j]=min(dist[i][j],dist[i][k]+dist[k][j]);
}

int main()
{
	scanf("%d",&n);
	for(int i=0;i<=n;i++)
		for(int j=0;j<=n;j++)
			scanf("%d",&dist[i][j]);
	floyd(); //floyd求出城市之间的最短距离
	for(int s=0;s<=(1<<n)-1;s++) //枚举访问城市集合s
		for(int i=1;i<=n;i++) //枚举最近访问过的城市
		{
			if(s&(1<<(i-1))) //当前访问集合中包含了第i个城市
			{
				if(s==(1<<(i-1))) //当前集合中只访问了第i个城市,则f=0到i的最短距离
					f[s][i]=dist[0][i];
				else //否则,在访问过的集合中找一个城市j,使得f[s-{i}][j]+dist[j][i]最小
				{
					f[s][i]=INF;
					for(int j=1;j<=n;j++)
					{
						if((s&(1<<(j-1)))&&j!=i) //找到集合中一个不是i的城市j
							f[s][i]=min(f[s][i],f[s^(1<<(i-1))][j]+dist[j][i]);
					}
				}
			}
		}
	int ans=INF;
	for(int i=1;i<=n;i++)
		ans=min(ans,f[(1<<n)-1][i]+dist[i][0]); //找到一个终点i,使得总旅行路程最短
	printf("%d\n",ans);
	return 0;
}

七、树型动态规划(树上动规)

这一类题目的特点是动规模型是树结构,动规决策时,一个节点的f值由它的子树节点决定,一般由其儿子和孙子节点决定,如果按顺序动规,一个节点的f值被访问的时间往往会比其子树节点的更早,解决方法有两种:1、自根到叶动规,依托DFS递归过程,先计算儿子节点的f值,回溯后再计算自己的f值,优点是简单,缺点是存储空间大,记录儿子节点需要邻接表或前向星,而且递归可能会出现爆栈的情况。2、自叶到根动规,即我为人人型动规或“刷表法”,由叶子节点向上访问其祖先节点,并更新其祖先的f值,优点是纯天然的动规,只需存储一个节点的父亲即可,存储空间小,缺点是写起来复杂,难以理解

1、Wikioi 1163 访问艺术馆

题目描述 Description

    皮尔是一个出了名的盗画者,他经过数月的精心准备,打算到艺术馆盗画。艺术馆的结构,每条走廊要么分叉为二条走廊,要么通向一个展览室。皮尔知道每个展室里藏画的数量,并且他精确地测量了通过每条走廊的时间,由于经验老道,他拿下一副画需要5秒的时间。你的任务是设计一个程序,计算在警察赶来之前(警察到达时皮尔回到了入口也算),他最多能偷到多少幅画。

第1行是警察赶到得时间,以s为单位。第2行描述了艺术馆得结构,是一串非负整数,成对地出现:每一对得第一个数是走过一条走廊得时间,第2个数是它末端得藏画数量;如果第2个数是0,那么说明这条走廊分叉为两条另外得走廊。数据按照深度优先得次序给出,请看样例

输出偷到得画得数量

60

7 0 8 0 3 1 14 2 10 0 12 4 6 2

2

s<=600

走廊的数目<=100

可以用f[i][j]表示在节点i及其子树偷j幅画的最少耗时,

 

八、其他类型的动态规划
1、Wikioi 1159 最大全0子矩阵

在一个0,1方阵中找出其中最大的全0子矩阵,所谓最大是指O的个数最多。

输入文件第一行为整数N,其中1<=N<=2000,为方阵的大小,紧接着N行每行均有N个0或1,相邻两数间严格用一个空格隔开。

输出文件仅一行包含一个整数表示要求的最大的全零子矩阵中零的个数。

5
0 1 0 1 0
0 0 0 0 0
0 0 0 0 1
1 0 0 0 0
0 1 0 0 0

9

这个题可用动规来做,具体思路如下:

1、读入矩阵

2、从第一行到最后一行,对于每一列i,求出h[i]=之前所有与i相连的连续0号方格数(包括自己),如下图中的左图

3、定义一个下界l[i]=1,r[i]=n,逐步增大l[i],减少r[i],使得l[i]最小,r[i]最大的同时,h[l[i]]>=h[i],h[i]<=h[r[i]],如下图中的右图

[NOIP复习]第三章:动态规划_第4张图片

4、绿色部分面积s=h[i]*(r[i]-l[i]+1),若s大于当前的解maxSqr,更新maxSqr,如下图的左图

5、最终所有行都遍历完,输出maxSqr,如下图中的右图

[NOIP复习]第三章:动态规划_第5张图片

 

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAXN 2200

int map[MAXN][MAXN]; //map[i][j]=(i,j)对应的数字
int h[MAXN]; //h[i]=从第一行一直到当前行,第i列连续的空格个数
int l[MAXN],r[MAXN]; //l[i]=>=h[i]的左边界,r[i]= >=h[i]的右边界

int main()
{
	int n,maxSqr=0;
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			scanf("%d",&map[i][j]);
	for(int i=1;i<=n;i++) //处理到了第i行
	{
		for(int j=1;j<=n;j++) //第j列
		{
			if(!map[i][j]) 
				h[j]++;
			else h[j]=0;
		}
		for(int j=1;j<=n;j++)
		{
			l[j]=j;
			while(l[j]>1&&h[j]<=h[l[j]-1])
				l[j]=l[l[j]-1];
		}
		for(int j=n;j>=1;j--)
		{
			r[j]=j;
			while(r[j]<n&&h[j]<=h[r[j]+1])
				r[j]=r[r[j]+1];
		}
		for(int j=1;j<=n;j++)
		{
			if(maxSqr<h[j]*(r[j]-l[j]+1))
				maxSqr=h[j]*(r[j]-l[j]+1);
		}
	}
	printf("%d\n",maxSqr);
	system("pause");
	return 0;
}

 

九、用数据结构优化动态规划
 1、POJ 2373 Dividing the Path(优先队列优化DP)

http://poj.org/problem?id=2373

题意:给定一组奶牛的活动范围[Li,Ri],以及草场的范围[L,R],每个喷水头的喷水半径可调节区间为[a,b],求至少要装多少个喷水头,使得每个奶牛的活动区间至少有一个喷水头

#include <iostream>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <queue>

#define MAXN 1000100
#define INF 0x3f3f3f3f

using namespace std;

struct node
{
    int f,x; //F[x]
    bool operator<(const node &b)const{return f>b.f;} //优先队列中f值越小的越优先
    node(int xx=0,int ff=0):x(xx),f(ff){}
}; //F[x]=覆盖[0,x]至少需要多少喷水头

priority_queue<node>pq; //保存解的优先队列

int n,l,a,b,F[MAXN],hasCow[MAXN]; //F[L]为答案

int main()
{
    scanf("%d%d%d%d",&n,&l,&a,&b);
    a*=2,b*=2;
    for(int i=1;i<=n;i++)
    {
        int s,e;
        scanf("%d%d",&s,&e);
        hasCow[s+1]++; //从s+1开始进入一个奶牛活动范围
        hasCow[e]--; //从e开始退出一个奶牛活动范围
    }
    int numOfCows=0; //numOfCows=这个点可能出没的奶牛个数
    for(int i=0;i<=l;i++)
    {
        F[i]=INF;
        numOfCows+=hasCow[i]; //加上活动范围从点i开始的奶牛个数
        hasCow[i]=numOfCows>0; //这时候hasCow[i]=该点是否有奶牛出现
    }
    for(int i=a;i<=b;i+=2) //初始化队列
    {
        if(!hasCow[i])
        {
            F[i]=1;
            if(i<=b+2-a) pq.push(node(i,1)); //如果在范围内,将这个状态入队
        }
    }
    for(int i=b+2;i<=l;i+=2)
    {
        if(!hasCow[i])
        {
            node now;
            while(!pq.empty())
            {
                now=pq.top();
                if(now.x<i-b) //队首状态的坐标不可能在喷头范围内
                    pq.pop();
                else
                    break;
            }
            if(!pq.empty()) F[i]=now.f+1;
        }
        if(F[i-a+2]!=INF) //队列中增加一个可达的状态
            pq.push(node(i-a+2,F[i-a+2]));
    }
    if(F[l]==INF) //无解
        cout<<-1<<endl;
    else
        cout<<F[l]<<endl;
    return 0;
}


 

 

 


 

 





你可能感兴趣的:([NOIP复习]第三章:动态规划)