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

时间:2024-07-20 21:04:08

一、背包问题

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

1、Wikioi 1014 装箱问题

题目描写叙述 Description

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

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

输入描写叙述 Input Description

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

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

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

输出描写叙述 Output Description

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

例子输入 Sample Input

24

6

8

3

12

7

9

7

例子输出 Sample Output

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 拦截导弹

题目描写叙述 Description

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

输入描写叙述 Input Description

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

输出描写叙述 Output Description

输出这套系统最多能拦截多少导弹。假设要拦截全部导弹最少要配备多少套这样的导弹拦截系统。

例子输入 Sample Input

389 207 155 300 299 170 158 65

例子输出 Sample Output

6

2

数据范围及提示 Data Size & Hint

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

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

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

#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

题目描写叙述 Description

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

n<=1000

输入描写叙述 Input Description

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

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

输出描写叙述 Output Description

输出可以获得的最大价值

例子输入 Sample Input

3

1 2 1

2 3 2

1 3 4

例子输出 Sample Output

4

数据范围及提示 Data Size & Hint

数据范围

对于40%的数据。n≤10。

对于100%的数据。n≤1000;

0<=ai,bi<=1000000

0<=ci<=1000000

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

首先须要把全部线段依据右端点升序排序,这种话,找与第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 石子合并

题目描写叙述 Description

有n堆石子排成一列,每堆石子有一个重量w[i], 每次合并可以合并相邻的两堆石子,一次合并的代价为两堆石子的重量和w[i]+w[i+1]。

问安排如何的合并顺序。可以使得总合并代价达到最小。

输入描写叙述 Input Description

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

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

输出描写叙述 Output Description

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

例子输入 Sample Input

4

4 1 1 4

例子输出 Sample Output

18

数据范围及提示 Data Size & Hint

这个题非常明显是一个区间型动规。用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 能量项链

题目描写叙述 Description

在Mars星球上,每一个Mars人都随身佩带着一串能量项链。

在项链上有N颗能量珠。

能量珠是一颗有头标记与尾标记的珠子。这些标记相应着某个正整数。而且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。

由于仅仅有这样,通过吸盘(吸盘是Mars人吸收能量的一种器官)的作用。这两颗珠子才干聚合成一颗珠子,同一时候释放出能够被吸盘吸收的能量。假设前一颗能量珠的头标记为m,尾标记为r,后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n(Mars单位),新产生的珠子的头标记为m,尾标记为n。

须要时。Mars人就用吸盘夹住相邻的两颗珠子,通过聚合得到能量,直到项链上仅仅剩下一颗珠子为止。

显然。不同的聚合顺序得到的总能量是不同的。请你设计一个聚合顺序。使一串项链释放出的总能量最大。

比如:设N=4,4颗珠子的头标记与尾标记依次为(2,3)
(3,5) (5。10)
(10。2)。我们用记号⊕表示两颗珠子的聚合操作,(j⊕k)表示第j。k两颗珠子聚合后所释放的能量。则第4、1两颗珠子聚合后释放的能量为:

(4⊕1)=10*2*3=60。

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

((4⊕1)⊕2)⊕3)=10*2*3+10*3*5+10*5*10=710。

输入描写叙述 Input Description

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

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

输出描写叙述 Output Description

仅仅有一行,是一个正整数E(E≤2.1*109)。为一个最优聚合顺序所释放的总能量。

例子输入 Sample Input

4

2 3 5 10

例子输出 Sample Output

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 过河卒

题目描写叙述 Description

 如图,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

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

输入描写叙述 Input Description

 键盘输入

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

输出描写叙述 Output Description

  屏幕输出

    一个整数(路径的条数)。

例子输入 Sample Input

 6 6 3 2

例子输出 Sample Output

17

数据范围及提示 Data Size & Hint

如描写叙述

这个题比較好做,仅仅要在棋盘中标明全部马的控制点,动规时避开它们就可以,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 传纸条

题目描写叙述 Description

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

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

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

输入描写叙述 Input Description

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

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

输出描写叙述 Output Description

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

例子输入 Sample Input

3 3

0 3 9

2 8 5

5 7 0

例子输出 Sample Output

34

数据范围及提示 Data Size & Hint

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复习]第三章:动态规划

如图。上面的图是单方向传纸条的过程。以下的图是双向传纸条的过程,非常明显以下的过程实际上相当于两个纸条同一时候向下传,并且其路径不能重合,这就是双线动规。能够开一个四维数组表示当前两张纸条分别所处的坐标(状态)。然后决策部分类似上面的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、骑士游历

题目描写叙述 Description

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

规定:

1)马仅仅能走日字

2)马仅仅能向右跳

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

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

输入描写叙述 Input Description

第一行2个整数n和m

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

输出描写叙述 Output Description

输出方案数

例子输入 Sample Input

30 30

1 15 3 15

例子输出 Sample Output

2

数据范围及提示 Data Size & Hint

2<=n,m<=50

相同是棋盘型动规。非常类似于过河卒,仅仅只是不须要推断是否有障碍物,而是决策变复杂了一点,只是也非常easy,不细说了,动规循环时,因为马仅仅能往右走,可是往上往下走都行,所以横坐标循环从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 乘积最大

题目描写叙述 Description

今年是国际数学联盟确定的“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设计一个程序,求得正确的答案。

输入描写叙述 Input Description

   程序的输入共同拥有两行:

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

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

输出描写叙述 Output Description

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

例子输入 Sample Input

4  2

1231

例子输出 Sample Output

62

数据范围及提示 Data Size & Hint

本题因为比較老。数据实际也比較小,用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 数的划分

题目描写叙述 Description

将整数n分成k份,且每份不能为空。随意两种划分方案不能同样(不考虑顺序)。

比如:n=7,k=3,以下三种划分方案被觉得是同样的。

1 1 5

1 5 1

5 1 1

问有多少种不同的分法。

输入描写叙述 Input Description

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

输出描写叙述 Output Description

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

例子输入 Sample Input

7 3

例子输出 Sample Output

4

数据范围及提示 Data Size & Hint

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

数据统计 Statistics

相同是划分型动态规划,只是这题须要一点数学技巧。用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 送外卖

题目描写叙述 Description

有一个送外卖的,他手上有n份订单,他要把n份东西。分别送达n个不同的客户的手上。n个不同的客户分别在1~n个编号的城市中。

送外卖的从0号城市出发,然后n个城市都要走一次(一个城市能够走多次),最后还要回到0点(他的单位),请问最短时间是多少。如今已知随意两个城市的直接通路的时间。

输入描写叙述 Input Description

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

接下来是一个(n+1)*(n+1)的矩阵,矩阵中的数均为不超过10000的正整数。

矩阵的i行j列表示第i-1号城市和j-1号城市之间直接通路的时间。当然城市a到城市b的直接通路时间和城市b到城市a的直接通路时间不一定同样,也就是说道路都是单向的。

输出描写叙述 Output Description

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

例子输入 Sample Input

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

例子输出 Sample Output

8

数据范围及提示 Data Size & Hint

1<=n<=15

这个题事实上就是TSP问题。能够用二进制数来表示一个状态集合,1表示訪问了该城市。0表示没有訪问过该城市,将集合表示为例如以下图

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

通过位运算来降低、添加集合中的元素,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秒的时间。你的任务是设计一个程序。计算在警察赶来之前(警察到达时皮尔回到了入口也算),他最多能偷到多少幅画。

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

输入描写叙述 Input Description

第1行是警察赶到得时间,以s为单位。

第2行描写叙述了艺术馆得结构,是一串非负整数,成对地出现:每一对得第一个数是走过一条走廊得时间,第2个数是它末端得藏画数量。假设第2个数是0,那么说明这条走廊分叉为两条另外得走廊。数据依照深度优先得次序给出,请看例子

输出描写叙述 Output Description

输出偷到得画得数量

例子输入 Sample Input

60

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

例子输出 Sample Output

2

数据范围及提示 Data Size & Hint

s<=600

走廊的数目<=100

能够用f[i][j]表示在节点i及其子树偷j幅画的最少耗时。

八、其它类型的动态规划

1、Wikioi 1159 最大全0子矩阵

题目描写叙述 Description

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

输入描写叙述 Input Description

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

输出描写叙述 Output Description

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

例子输入 Sample Input

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

例子输出 Sample Output

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、绿色部分面积s=h[i]*(r[i]-l[i]+1),若s大于当前的解maxSqr。更新maxSqr,例如以下图的左图

5、终于全部行都遍历完,输出maxSqr,例如以下图中的右图

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

#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;
}