动态规划32讲

基本概念

动态规划是一种在数学、计算机科学和经济学中使用的,通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划常常适用于有重叠子问题和最优子结构性质的问题,用时往往少于朴素解法。思路基本上为:解其不同部分(即子问题),再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量。

例如,假设递推式F(n)=F(n-1)+F(n-2),其为最优化问题:如下图所示的数塔,从顶部出发,在每一结点可以选择向左走或是向右走,一直走到底层,要求找出一条路径,使路径上的值最大(或最小)。若采用枚举法(暴力思想):在数塔层数稍大的情况下(如31),则需要列举出的路径条数将是一个非常庞大的数目(2^30=1024^3 > 10^9=10亿)。

则需要考虑动态规划:f[i][j] = max(f[i-1][j-1]],f[i-1][j])+a[i][j]

一,动态规划三要素:阶段,状态,决策

【理解】将动态规划的求解过程类比为一个工厂的生产线,阶段就是生产某个商品的不同的环节,状态就是工件当前的形态,决策就是对工件的操作。显然不同阶段是对产品的前面各个状态的小结,由一个个的小结构成了最终的整个生产线。每个状态间又有关联(下一个状态是由上一个状态做出某个决策后产生的)。

【用图论理解动态规划】把动态规划中的状态抽象成一个点,在有直接关联的状态间连一条有向边,状态转移的代价就是边上的权。这样就形成了一个有向无环图AOE网。对这个图进行拓扑排序,删除一个边后同时出现入度为0的状态在同一阶段。这样对图求最优路径就是动态规划问题的求解。

一个状态经过一个决策变成了另外一个状态,这个过程就是状态转移,用来描述状态转移的方程就是状态转移方程。

二,动态规划的适用范围

动态规划用于解决多阶段决策最优化问题,但是不是所有的最优化问题都可以用动态规划解答呢?

一般在题目中出现求最优解的问题就要考虑动态规划了,但是否可以用还要满足三个条件:1)最优子结构(最优化原理);2)无后效性;3)子问题的重叠性

【最优化原理】一个最优化策略的子策略总是最优的。

【无后效性】对于某个给定的阶段状态,它以前各阶段的状态无法直接影响它未来的决策,而只能通过当前的这个状态。在状态i求解时用到状态j,而状态j求解时用到状态k…..状态N,而求状态N时用到了状态i,这样求解状态的过程形成了环就无法用动态规划解答,这也是上面用图论理解动态规划中形成的图无环的原因。即要求当前状态是前面状态的完美总结,现在与过去无关,当然,有时换一个划分状态或阶段的方法就满足无后效性了,这样的问题仍然可以用动态规划解。

【子问题的重叠性】动态规划将原来具有指数级时间复杂度的搜索算法改进成了具有多项式时间复杂度的算法。其中的关键在于解决冗余,这是动态规划算法的根本目的。

三,动态规划解决问题的一般思路

第一步:判断该问题是否可用动态规划解决,若不能就需考虑搜索或贪心;

第二步:当确定问题可以用动态规划求解后,按照以下方法解决问题:
(1)模型匹配法:
          最先考虑的方法。挖掘问题的本质,如果发现问题是自己熟悉的某个基本的模型,就直接套用,但要小心其中的一些小的变动。
(2)三要素法:
          仔细分析问题尝试着确定动态规划的三要素,不同问题的确定方向不同:
          1)先确定阶段的问题:数塔问题,和走路问题;
          2)先确定状态的问题:大多数都先确定状态;
          3)先确定决策的问题:背包问题。
(3)寻找规律法:
          耐心推几组数据后,寻找规律,总结规律间的共性,类似于贪心。
(4)边界条件法
          找到问题的边界条件,然后考虑边界条件与它的邻接状态之间的关系。
(5)放宽约束和增加约束
          给问题增加一些条件或删除一些条件使问题变的清晰。

背包问题

【基本模型】现有N个物品,每个物品的价值为V,重量为W。求用一个载重量为S的背包装这些物品,使得所装物品的总价值最高。

【分类】(1)小数背包:物品的重量是实数,如油,水等可以取1.67升;
               (2)整数背包:<1> 0/1背包:每个物品只能选一次,或不选,不能只选一部分;
                                              <2> 部分背包:所选物品可以只选一部分;
               (3)多重背包:背包不只一个。

【简化】现有N个物品,每个物品重量为W[i],这些物品能否使载重量为S的背包装满,即重量和正好为S,如果不能那能使物品的重量和最重达到多少? \rightarrow 针对这一问题我们以物品的个数分阶段,设计一个状态opt[i]表示载重量为i的背包可否装满,显然opt[i]的基类型是Boolean。

【决策】当要装第i个物品时,如果前i-1个物品可以使载重为k的背包装满,那么载重为k+w[i]的背包就可以装满,于是对于一个opt[j]来说,只要opt[j-w[i]]为true(表示可装满),那opt[j]就可以装满。但要注意:针对每一个物品枚举背包的载重量时,如果这样正向地推导,会使同一个物品用好几次,因为k+w[i]可装满,那k+w[i]+w[i]就可装满,但实际上是装不满的,因为物品只有一个,解决这个问题很简单,只需逆向推导即可。

【无后效性】显然对于每一个阶段,都是独立的,物品的顺序并不影响求解,因为装物品的次序不限。而对于opt[j]只考虑opt[j-w[i]]而不考虑后面的,所以满足无后效性。

【状态转移方程】opt[j]:=opt[j-w[1]]  {opt[j-w[i]]=true}

【时间复杂度】阶段数O(S)*状态数O(N)*转移代价O(1)=O(SN)

01背包

【基本模型】有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【特点】每种物品仅有一件,可以选择放或不放。

【状态转移方程】f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}

【空间优化】

for i=1..N
    for v=0..V
        f[v]=max{f[v],f[v-c[i]]+w[i]};
// 但以上会导致物品重复购买
for i=1..N
    for v=V..0
        f[v]=max{f[v],f[v-c[i]]+w[i]};

【复杂度】

空间复杂度:O(V)

完全背包

【基本模型】有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【状态转移方程】f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}

【复杂度】

与01背包问题一样,有O(N*V)个状态需要求解。求解状态f[i][v]的时间是O(v/c[i]),总的复杂度是超过O(VN)的

【优化】

把第i种物品拆成费用为c[i]*2^k、价值为w[i]*2^k的若干件物品,其中k满足c[i]*2^k<V

  • 二进制的思想
  • 这样把每种物品拆成O(log(V/c[i]))件物品,得到了更优的O(VN)的算法。

多重背包

【基本模型】有N种物品和一个容量为V的背包。第i种物品最多有n[i]件可用,每件费用是c[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

【特点】这题目和完全背包问题很类似。基本的方程只需将完全背包问题的方程略微一改即可,因为对于第i种物品有n[i]+1种策略:取0件,取1件……取n[i]件。

【状态转移方程】令f[i][v]表示前i种物品恰放入一个容量为v的背包的最大权值,则:f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=n[i]}。

背包简化

动态规划的本质就是递推,动规涉及最优决策,递推是单纯总结,背包问题的简化版准确地说是递推而非动态规划,至于动归和递推之间的界线本就模糊。

   在无法确定为0/1背包的情况下,用——三要素法:

  • 决策:当题目明确说明对于一个物品要么选要么不选,则决策就是不选(用0表示)或选(用1表示)。确定决策后,再通过搜索求解。
  • 阶段:显然,面对一堆物品,一次装一个入背包,那么阶段即为物品的个数。
  • 状态:有人在装东西时有个习惯——先把东西分类,然后把同类的东西打个小包,最后再把小包放进去。即可按这个思路给物品打一些小包,按照单位为1的递增顺序准备许多小包,比如载重量是6的背包,可以为它准备重量是1,2,3,4,5的小包。因此,可设计状态opt[i,j]表示装第i个物品时载重为j的包可以装到的最大价值,状态转移方程(w[i]:第i个物品的重量,v[i]:第i个物品的价值):opt[i,j]=\left\{\begin{array}{lr}opt[i-1,j]\:(j-w[i]<0,i>0) & \\ max\left \{ opt[i-1,j],opt[i-1,j-w[i]]+v[i] \right \}\:(j-w[i]\geq 0,i>0) & \end{array} \right.,解释:要载重为j的背包空出w[i](j-w[i])的空间且装上第i个物品,比不装获得的价值大就装上它。边界条件:opt[0,i]=0; (i∈{1..S})。

   【放宽/增加约束】1)放宽约束——不考虑价值,求最优解(背包问题的简化版);2)增加约束——背包只能装满。 显然,对于N个装满背包的方案中只要找到一个价值最大的就是问题的解,对于装不满的情况,其总要达到一定的重量X,即可将此情况看作是装满一个载重为X的背包。【放宽约束:便于找到问题的突破口——和背包问题简化版一样,可以确定载重量为S的背包是否可以装满;增加约束:便于找到问题的求解方法——在装满背包的方案中选择最优的一个方案】
    这样,设计一个状态opt[j]表示装满载重为j的背包可获得的最大价值。对于第i个物品,只要opt[j-w[i]]可以装满且opt[j-w[i]]+v[i]比opt[j]大就装上这个物品(更新opt[j])。

   如何使得opt[j]既有“是否构成”又有“最优”的概念:

  • opt[j]只表示最优,只不过使初始条件+1,判断opt[j]是否为0,如果opt[j]=0说明j装不满;
  • 边界条件:opt[0]=1;
  • 状态转移方程:opt[j]=max{opt[j-w[i]]} (0<i<n,w[i]<=j<=S);
  • 问题解:ans=max{opt[i]}-1 (0<i<=S)。

【时间复杂度】阶段数O(S)*状态数O(N)*转移代价O(1)=O(SN)

二维费用的背包

二维费用的背包问题是指:对于每件物品,具有两种不同的费用;选择这件物品必须同时付出这两种代价;对于每种代价都有一个可付出的最大值(背包容量)。问怎样选择物品可以得到最大的价值。设这两种代价分别为代价1和代价2,第i件物品所需的两种代价分别为a[i]和b[i]。两种代价可付出的最大值(两种背包容量)分别为V和U。物品的价值为w[i]。

费用加了一维,只需状态也加一维即可。设f[i][v][u]表示前i件物品付出两种代价分别为v和u时可获得的最大价值。状态转移方程就是:f[i][v][u]=max{f[i-1][v][u],f[i-1][v-a[i]][u-b[i]]+w[i]}。

有依赖的背包

【特点】这种背包问题的物品间存在某种“依赖”的关系。也就是说,i依赖于j,表示若选物品i,则必须选物品j。

二维状态

opt[i,j]的i,j可代表:

(1)i,j组合代表一个点的坐标(显然是平面坐标系,如:街道问题)

(2)i,j组合表示一个矩阵某元素的位置(第i行第j列,如:数塔问题)

(3)起点为i长度为j的区间。(如:回文词)

(4)起点为i终点为j的区间。(如:石子合并问题)

(5)两个没关联的事物,事物1的第i个位置,对应事物2的第j个位置(如:花店橱窗设计问题)

(6)两个序列,第一个序列的前i个位置或第i个位置,对应第2个序列的前j个位置或第j个位置。(如:最长公共子序列问题)

(7)其它

多维状态

多维动态规划以三、四维最为常见,大多都是基于一、二维加一些条件,或是在多进程动态规划中用到。当然除了这些特点外,状态的表示也有一些共性。

  • 三维:状态opt[i,j,k]一般可表示:

(1)二维状态的基础上加了某个条件,或其中一维变成两个。比如opt[L,i]表示起点为i,长度为L的序列的最优值;opt[L,i,j]就可表示起点是i和起点是j,长度是L的两个序列的最优值。
(2)i,j,k组合表示一个正方形(某角端点为(i,j),边长为K)。
(3)i,j,k组合表示一个等边三角形(某角端点为(i,j),边长为K)。

  • 四维:除了二、三维加条件外,还可以用i,j,k,t组合来描述一个矩形((i,j)点和(k,t)点是两个对顶点)。

多维状态动态规划的优化

一般多维动态规划的空间复杂度较高,需要考虑动态规划的优化方法。

  • 常见空间优化:滚动数组——若一个状态的决策的步长为N就只保留已求出的最后N(一般N=1)个阶段的状态,因为当前状态只和后N个阶段中的状态有关,更以前的已经利用过了可以替换掉。注意最好只让下标滚动(会更省时)。例如:X:=K1,K1:=K2,K2:=K3,K3:=X,这样就实现了一个N=3的下标滚动,在滚动完如果状态涉及累加/乘类的操作要注意将当前要求的状态初始化。
  • 常见时间优化:利用一些数据结构(堆、并查集,HASH)降低查找复杂度。
  • 时间空间双重优化:改变状态的表示法,降低状态维数。

多进程动态规划

所谓多进程就是在原问题的基础上,要求将这个问题重复多次的总和最大。

树型动态规划

由于树型动态规划比较特殊,要借助树这种数据结构,所以很多地方都把它独立看做一类。一般树型动态规划都是建立在二叉树上的,对于普通的树和森林要把它们转化成二叉树。树型动态规划是动态规划类问题中最好分析的,因为树本身就是一个递归的结构,阶段、状态、决策都很明显,子问题就是子树的最优值,也很明显。

较其他类型的动态规划不同的是,求解树型动态规划的第一步一般要先建立数据结构,考虑问题的数据结构是二叉树呢?还是多叉树呢?还是森林呢?

其他类型的动态规划一般都是用逆向递推实现,而树型动态规划一般要正向求解,为了保证时间效率要进行记忆化(即常说的记忆化搜索)。

树型动态规划的三要素:

阶段:树的层数
状态:树中的结点
决策:某个子数的最优,或所有子树的最优和,或某几个子树的最优

通过上面的决策,发现如果是一棵多叉树,针对求某几个(n>=1)子树的最优解,决策会很多。以至于我们没法写出准确的状态转移方程。这就是为什么要把多叉树转化成二叉树的原因(当然,如果问题是求所有子树的和,就没必要转化,如URAL-1039 没有上司的舞会)。如果把多叉树或森林转化成二叉树,要注意:左儿子与根结点是父子关系,而右儿子在原问题中与根结点是兄弟关系。

//用邻接矩阵存多叉树,转化成二叉树的部分代码
//G:存图
//F[i]:第i个结点的儿子应该插入的位置
//W:结点的值
//BT:二叉树
Procedure creat_tree(T:tree);
 Var
   i:longint;
Begin
  for i:=1 to n do
   Begin
    for j:=1 to n do
     If g[i,j] then
      Begin
       If  F[i]=0 then
        BT[i].L:=j
       Else  BT[F[i]].r:=j;
       F[i]:=j;
      End;
   End;
End;

例题:最长下降子序列问题-合唱队形

【问题描述-poj2711】
N位同学站成一排,音乐老师要请其中的(N-K)位同学出列,使得剩下的K位同学排成合唱队形,满足:设K位同学从左到右依次编号为1,2…,K,他们的身高分别为T1,T2,…,TK,  则他们的身高满足T1<...<Ti>Ti+1>…>TK(1<=i<=K)。现已知所有N位同学的身高,计算最少需要几位同学出列,可以使得剩下的同学排成合唱队形。

【输入文件】
    输入文件chorus.in的第一行是一个整数N(2<=N<=100),表示同学的总数。第一行有n个整数,用空格分隔,第i个整数Ti(130<=Ti<=230)是第i位同学的身高(厘米)。
【输出文件】
    输出文件chorus.out包括一行,这一行只包含一个整数,就是最少需要几位同学出列。
【样例输入】
8
186 186 150 200 160 130 197 220
【样例输出】
4
【数据规模】
对于50%的数据,保证有n<=20;
对于全部的数据,保证有n<=100。

【问题分析】

分别从前往后,从后往前做最长上升子序列,最后扫一遍,寻找两种序列和长度最大的值。类似的问题:最长下降子序列、最长上升子串、最长公共子串。

出列人数最少,也就是说留下的人最多,即序列最长。这样分析就是典型的最长下降子序列问题。只要枚举每一个人站中间时可以得到的最优解。显然它就等于,包括他在内向左求最长上升子序列,向右求最长下降子序列。

【复杂度分析】

计算最长下降子序列的复杂度是O(N^2),该题一共得求N次,总复杂度是O(N^3)。该复杂度对于此题的数据范围来说可以AC,但有更好的方法:最长子序列只要一次即可,因为最长上升/下降子序列不受中间人的影响。只要分别用OPT1[i]求一次最长上升子序列,OPT2[i]求一次最长下降子序列。这样答案就是N-max(opt1[i]+opt2[i]-1), 复杂度由O(N^3)降到了O(N^2)。

program chorus;
var
	i,j,ans,n:longint;
	a,s,f:array[0..100000]of longint;
begin
	assign(input,'input.txt'); reset(input);
	assign(output,'output.txt'); rewrite(output);
	readln(n);  ans:=0;
	for i:=1 to n do
	begin
		read(a[i]);
		f[i]:=1; s[i]:=1;
	end; readln;
	a[0]:=maxlongint;
	for i:=1 to n do
	begin
		for j:=i-1 downto 0 do
		begin
			if (a[j]<a[i])and(f[j]+1>f[i]) then
				f[i]:=f[j]+1;
		end;
	end;
	a[n+1]:=maxlongint;
	for i:=n downto 1 do
	begin
		for j:=i+1 to n+1 do
		begin
			if (a[j]<a[i])and(s[j]+1>s[i]) then
				s[i]:=s[j]+1;
		end;
	end;
	for i:=1 to n do
	if f[i]+s[i]>ans then ans:=f[i]+s[i];
	writeln(n-ans+1);
	close(input);close(output);
end

例题:逢低吸纳

【问题描述】
“逢低吸纳”是炒股的一条成功秘诀。如果你想成为一个成功的投资者,就要遵守这条秘诀:"逢低吸纳,越低越买",即每次你购买股票时的股价一定要比你上次购买时的股价低,按照这个规则购买股票的次数越多越好,看看你最多能按这个规则买几次。
给定连续的N天中每天的股价。你可以在任何一天购买一次股票,但是购买时的股价一定要比你上次购买时的股价低。写一个程序,求出最多能买几次股票。以下面这个表为例, 某几天的股价是:
天数 1    2   3   4   5    6   7   8    9  10 11 12 
股价 68 69 54 64 68 64 70 67 78 62 98 87 
此例中,聪明的投资者如果每次买股票时的股价都比上一次买时低,那么他最多能买4次股票。比如一种买法为:
天数 2    5   6  10
股价 69 68 64 62

【输入文件】buylow.in
第1行: N (1 <= N <= 5000), 表示能买股票的天数。
第2行以下: N个正整数 (可能分多行) ,第i个正整数表示第i天的股价. 这些正整数大小不会超过longint(pascal)/long(c++)。
【输出文件】buylow.out
只有一行,输出两个整数:能够买进股票的天数长度,达到这个值的股票购买方案数量。
在计算解的数量的时候,如果两个解所组成的字符串相同,那么这样的两个解被认为是相同的(只能算做一个解)。因此,两个不同的购买方案可能产生同一个字符串,这样只能计算一次。
【输入样例】
12 
68 69 54 64 68 64 70 67 78 62 98 87 
【输出样例】
4 2
【提交链接】
http://train.usaco.org/

【问题分析】

此题为最长下降子序列问题,对于第一问,以天数为阶段,设计状态opt[i]表示前i天中要买第i天的股票所能得到的最大购买次数。状态转移方程:opt[i]=max(opt[j])+1  (a[i]\geqa[j],0\leqj<i)  {a[i]存第i天股价},其中最大的opt[i]就是最终的解。

对于第二问,从问题的边界出发来考虑,当第一问求得一个最优解opt[i]=X时,在1到N中若还有另外一个opt[j]=X,那么选取j的这个方案肯定也是成立的。那是不是统计所有opt[j]=X的个数就是解了呢?显然没那么简单,因为选取j这天的股票构成的方案数不一定等于1,比如:

5 6 4 3 1 2
方案一:5431
方案二:5432
方案三:6431
方案四:6432
X=4

其中opt[5]=X和opt[6]=X,opt[i]=X的个数只有两个,而实际上应该有四个方案,但再仔细观察发现,构成opt[5]=X的这个方案中,opt[j]=X-1的方案数有两个,opt[j]=X-2的有一个,opt[j]=X-3的有一个……显然这是满足低归定义的设计函数F(i),F(i)表示前i张中用到第i张股票的方案数。递推式:

F(i)=\left\{\begin{array}{lr}1\:(i=0) & \\ Sum(F(j))\:(0\leq j\leq n,a[j]>a[i],opt[j]=opt[i]-1) & \end{array} \right. {Sum(\cdot )=\sum \cdot}

Result=Sum(F(j))\:(0<j\leq n,opt[j]=X)

【复杂度分析】

求解第一问时间复杂度是O(N^2),求解第二问如果用递推或递归+记忆化,时间复杂度仍为O(N^2),但若用赤裸裸的递归会复杂得多,因为那样会造成好多不必要的计算。

另外,还要注意:1)如果有两个方案中出现序列一样,视为一个方案,则需要加一个数组next用next[i]记录和第i个数情况一样(即opt[i]=opt[j]且a[i]=a[j])可看做一个方案的最近的位置,递推时j只要走到next[i]即可;2)为了方便操作可以将a[n+1]赋值为-maxlongint,这样可以认为第n+1个一定可以买,答案就是Sum(F(n+1));3)该题数据规模,需考虑高精度。

{
ID:hhzhaojia2
PROG:buylow
LANG:PASCAL
}
program buylow;
const
 fin='buylow.in';
 fout='buylow.out';
 maxn=5010;
 maxsize=10;
 jz=100000000;
type
 arrtype=array[0..maxsize] of longint;
var
 a,opt,next:array[0..maxn] of longint;
 F:array[0..maxn] of arrtype;
 n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(n);
  if n=5 then                {USACO上最后一点有误,程序中需做处理}
   begin
    writeln('2 5');
    close(input);
    close(output);
    halt;
   end;
  for i:=1 to n do
   read(a[i]);
 end;
procedure Hinc(var x:arrtype;y:arrtype);
 var
  i,z:longint;
 begin
  z:=0;
  for i:=1 to maxsize do
   begin
    z:=z div jz+x[i]+y[i];
    x[i]:=z mod jz;
   end;
 end;
procedure main;
 var
  i,j:longint;
 begin
  a[0]:=maxlongint;
  a[n+1]:=-maxlongint;
  for i:=1 to n+1 do
   for j:=0 to i-1 do
    if (a[j]>a[i]) and (opt[j]+1>opt[i])  then
     opt[i]:=opt[j]+1;
  for i:=1 to n do
    begin
     j:=i-1;
     while (j>0) and ((opt[i]<>opt[j]) or (a[i]<>a[j])) do
      dec(j);
     next[i]:=j;
    end;
  F[0,1]:=1;
  for i:=1 to n+1 do
   for j:=i-1 downto next[i] do
    if (opt[j]=opt[i]-1) and (a[j]>a[i]) then
     Hinc(F[i],F[j]);
 end;
procedure print;
 var
  i,top,m:longint;
 begin
  write(opt[n+1]-1,' ');
  top:=maxsize;
  while (top>1) and (F[n+1][top]=0) do
   dec(top);
  write(F[n+1,top]);
  for i:=top-1 downto 1 do
   begin
    m:=F[n+1,i];
    while m<maxsize div 10 do
     begin
      write('0');
      m:=m*10;
     end;
    write(F[n+1,i]);
   end;
  writeln;
  close(input);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:船

【问题描述】
PALMIA国家被一条河流分成南北两岸,南北两岸上各有N个村庄。北岸的每一个村庄有一个唯一的朋友在南岸,且他们的朋友村庄彼此不同。每一对朋友村庄想要一条船来连接他们,他们向政府提出申请以获得批准。由于河面上常常有雾,政府决定禁止船只航线相交(如果相交,则很可能导致碰船)。你的任务是编写一个程序,帮助政府官员决定批准哪些船只的航线,使得不相交的航线数目最大。
【输入文件】ships.in
输入文件由几组数据组成。每组数据的第一行有2个整数X,Y,中间有一个空格隔开,X代表PALMIA河的长度(10<=X<=6000),Y代表河的宽度(10<=Y<=100)。第二行包含整数N,表示分别坐落在南北两岸上的村庄的数目(1<=N<=5000)。在接下来的N行中,每一行有两个非负整数C,D,由一个空格隔开,分别表示这一对朋友村庄沿河岸与PALMIA河最西边界的距离(C代表北岸的村庄,D代表南岸的村庄),不存在同岸又同位置的村庄。最后一组数据的下面仅有一行,是两个0,也被一空格隔开。
【输出文件】ships.out
对输入文件的每一组数据,输出文件应在连续的行中表示出最大可能满足上述条件的航线的数目。
【输入样例】
30 4
7
22 4
2 6
10 3
15 12
9 8
17 17
4 2
0 0
【输出样例】
4

【问题分析】

方法一:寻找规律法
例如某一合法航线安排为:
                                C1  C2  C3  C4
北岸红线的端点: 4     9    15   17
南岸红线的端点: 2     8    12   17
                                D1  D2  D3  D4
明显地,无论各航线斜率如何,均满足以下规律:
C1<C2<C3<C4且D1<D2<D3<D4
如果把输入数据按升序排序,问题可抽象为:在一个序列D里找到最长的序列满足Di<Dj<Dk……且i<j<k,即典型的最长非降子序列问题。

方法二:边界条件法(缩小数据规模后考虑问题)
N=1时,Result=1;
N=2时,考虑如下情况
    C1 D1
    C2 D2
当C1<C2时,如果D1>D2,那么一定会相交,反之则不会相交;
当C1>C2时,如果D1<D2,那么一定会相交,反之则不会相交。
N=3时,考虑如下情况
    C1 D1
    C2 D2
    C3 D3
……
对于任意两条航线,如果满足Ci<Cj且Di<Dj,则两条航线不相交。若在一个序列里,所有航线都不相交,那必然满足:C1<C2<C3…Cans且D1<D2<D3…<Dans,也就是将C排序后求出最长的满足这个条件的序列的长度就是解,即最长非降子序列问题。

【复杂度分析】

排序可用O(nlogn)的算法,求最长非降子序列的时间复杂度是O(n^2),则总时间复杂度为O(n^2)。

program ships;
const
 fin='ships.in';
 fout='ships.out';
 maxn=5010;
type
 retype=record
         C,D:longint;
        end;
var
 x,y,n,ans:longint;
 a:array[0..maxn] of retype;
 opt:array[0..maxn] of longint;
procedure init;
 var
  i:longint;
 begin
  readln(n);
  for i:=1 to n do
   read(a[i].c,a[i].d);
 end;
procedure quick(L,r:longint);
 var
  i,j,x:longint;
  y:retype;
 begin
  i:=L;
  j:=r;
  x:=a[(i+j) div 2].c;
  repeat
   while a[i].c<x do
    inc(i);
   while a[j].c>x do
    dec(j);
   if i<=j then
    begin
     y:=a[i];
     a[i]:=a[j];
     a[j]:=y;
     inc(i);
     dec(j);
    end;
  until i>j;
  if j>L then quick(L,j);
  if i<r then quick(i,r);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  quick(1,n);
  for i:=1 to n do
   for j:=0 to i-1 do
    if (a[j].d<a[i].d) and (opt[j]+1>opt[i]) then
     opt[i]:=opt[j]+1;
  ans:=-maxlongint;
  for i:=1 to n do
   if ans<opt[i] then
    ans:=opt[i];
  writeln(ans);
 end;
begin
 assign(input,fin);
 reset(input);
 assign(output,fout);
 rewrite(output);
 read(x,y);
 while (x+y<>0) do
  begin
   init;
   main;
   read(x,y);
  end;
 close(input);
 close(output);
end.

例题:背包问题-装箱问题

【问题描述】
    有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每个物品有一个体积(正整数)。要求n个物品中,任取若干个装入箱内,使箱子的剩余空间为最小。
【输入文件】
    第一行:一个正整数V表示箱子的容量;第二行:一个正整数n表示物品个数,接下来n行列出这n个物品各自的体积。
【输出文件】
    单独一行,表示箱子最小的剩余空间。
【输入样例】
24
6
8
3
12
7
9
7
【输出样例】
0

【问题分析】

本题为经典的0/1背包问题,且是简化版,箱子即背包,容量即载重量,体积即重量,剩余空间最小即尽量装满背包:有一个载重量为V的背包,有N个物品,尽量多装物品,使背包尽量的重。
设计一个状态opt[i]表示重量i可否构成。状态转移方程:opt[j]:=opt[j-w[1]]  {opt[j-w[i]]=true}
最终的解就是V-x(x<=n且opt[x]=true且opt[x+1..n]=false)。

program packing;
var
	i,j,w,v,n:longint;
	f:array[0..100000]of longint;
begin
	assign(input,'input.txt'); reset(input);
	assign(output,'output.txt'); rewrite(output);
	readln(v);
	readln(n);
	for i:=1 to n do
	begin
		readln(w);
		for j:=v downto w do
		begin
			if f[j-w]+w>f[j] then f[j]:=f[j-w]+w;
		end;
	end;
	writeln(v-f[v]);
	close(input); close(output);
end.

例题:背包问题-砝码称重

【问题描述】
    设有1g、2g、3g、5g、10g、20g的砝码各若干枚(其总重<=1000),用他们能称出的重量的种类数。
【输入文件】
    a1  a2  a3  a4  a5  a6(表示1g砝码有a1个,2g砝码有a2个,…,20g砝码有a6个,中间有空格)。
【输出文件】
    Total=N(N表示用这些砝码能称出的不同重量的个数,但不包括一个砝码也不用的情况)。
【输入样例】
    1 1 0 0 0 0
【输出样例】
    TOTAL=3

【问题分析】

问题可转化为:已知a1+a2+a3+a4+a5+a6个砝码的重量分别为w[i],w[i]∈{1,2,3,5,10,20},其中砝码重量可以相等,求用这些砝码可称出的不同重量的个数。即为经典的0/1背包问题的简化版,只是本题不是求最大载重量,是统计所有的可称出的重量的个数。

program fmcz;
const d:array[1..6]of 1..20=(1,2,3,5,10,20);
      maxll=1001;
var
	i,j:longint;
	a,b:array[1..10]of longint;
	f:array[1..1001]of longint;
begin
	for i:=1 to 6 do
	begin
		read(a[i]);
		b[i]:=a[i]*d[i];
	end;
	readln;
	for i:=1 to 6 do
	begin
		for j:=maxll downto b[i] do
		begin
			if f[j-b[i]]+b[i]>f[j] then f[j]:=f[j-b[i]]+b[i];
		end;
	end;
	writeln(f[maxll]);
end.

例题:背包问题-积木城堡

【问题描述】
    SY的儿子小SY最喜欢玩的游戏是用积木垒漂亮的城堡。城堡是用一些立方体的积木垒成的,城堡的每一层是一块积木。小SY是一个比他爸爸SY还聪明的孩子,他发现垒城堡的时候,如果下面的积木比上面的积木大,那么城堡便不容易倒。所以他在垒城堡的时候总是遵循这样的规则。
    小SY想把自己垒的城堡送给幼儿园里漂亮的女孩子们,这样可以增加他的好感度。为了公平起见,他决定送给每个女孩子一样高的城堡,这样可以避免女孩子们为了获得更漂亮的城堡而引起争执。可是他发现自己在垒城堡的时候并没有预先考虑到这一点。所以他现在要改造城堡。由于他没有多余的积木了,他灵机一动,想出了一个巧妙的改造方案。他决定从每一个城堡中挪去一些积木,使得最终每座城堡都一样高。为了使他的城堡更雄伟,他觉得应该使最后的城堡都尽可能的高。
    任务:请你帮助小SY编一个程序,根据他垒的所有城堡的信息,决定应该移去哪些积木才能获得最佳的效果。
【输入文件】
    第一行是一个整数N(N<=100),表示一共有几座城堡。以下N行每行是一系列非负整数,用一个空格分隔,按从下往上的顺序依次给出一座城堡中所有积木的棱长。用-1结束。一座城堡中的积木不超过100块,每块积木的棱长不超过100。
【输出文件】
    一个整数,表示最后城堡的最大可能的高度。如果找不到合适的方案,则输出0。
【输入样例】
2
2 1 –1
3 2 1 -1
【输出样例】
3
【提交链接】
http://www.vijos.cn/

【问题分析】

首先要说明一点,本题可以挪走任意一个积木,不一定是最上面的。问题转化:搭好的积木再拿走就相当于当初搭的时候没选的积木。把积木可搭建的最大高度看做背包的载重量,每块积木的高度就是物品的重量,也就是用给定的物品装指定的包,使每个包装的物品一样多,且在符合条件的前提下尽量多,即经典的背包问题:对于每一个城堡求一次,最终找到每一个城堡都可达到的最大高度即可。

program P1095;
const
 maxhig=7000;
 maxn=100;
var
 n,top:longint;
 opt:array[0..maxn,0..maxhig] of boolean;
 a:array[0..maxn] of longint;
procedure init;
 var
  i:longint;
 begin
  readln(n);
  fillchar(opt,sizeof(opt),false);
  for i:=1 to n do
   opt[i,0]:=true;
 end;
function can(m:longint):boolean;
 var
  i:longint;
 begin
  can:=true;
  for i:=1 to n do
   if not opt[i,m] then
    exit(false);
 end;
procedure main;
 var
  ii,m,tothig,i,j,ans:longint;
 begin
  for ii:=1 to n do
   begin
    top:=0;
    read(m);
    tothig:=0;
    while m>0 do
     begin
      inc(top);
      a[top]:=m;
      inc(tothig,m);
      read(m);
     end;
    for i:=1 to top do
     for j:=tothig downto 1 do
      if (j-a[i]>=0) and (opt[ii,j-a[i]]) then
       opt[ii,j]:=true;
   end;
  ans:=maxhig;
  while not opt[1,ans] do 
dec(ans);
  while not can(ans) do
    dec(ans);
  writeln(ans);
 end;
begin
 init;
 main;
end.

例题:背包问题-采药

【问题描述】
    辰辰是个天资聪颖的孩子,他的梦想是成为世界上最伟大的医师。为此,他想拜附近最有威望的医师为师。医师为了判断他的资质,给他出了一个难题。医师把他带到一个到处都是草药的山洞里对他说:“孩子,这个山洞里有一些不同的草药,采每一株都需要一些时间,每一株也有它自身的价值。我会给你一段时间,在这段时间里,你可以采到一些草药。如果你是一个聪明的孩子,你应该可以让采到的草药的总价值最大。”如果你是辰辰,你能完成这个任务吗?
【输入文件】
    输入文件medic.in的第一行有两个整数T(1 <= T <= 1000)和M(1 <= M <= 100),用一个空格隔开,T代表总共能够用来采药的时间,M代表山洞里的草药的数目。接下来的M行每行包括两个在1到100之间(包括1和100)的整数,分别表示采摘某株草药的时间和这株草药的价值。
【输出文件】
    输出文件medic.out包括一行,这一行只包含一个整数,表示在规定的时间内,可以采到的草药的最大总价值。
【输入样例】
70 3
71 100
69 1
1 2
【输出样例】
    3
【数据规模】
对于30%的数据,M <= 10;
对于全部的数据,M <= 100。

【问题分析】

本题为典型的0/1背包问题,把采摘时间看做标准模型中的重量,把规定的时间看做载重量为T的背包。

//二维状态
program medic;
const
 fin='medic.in';
 fout='medic.out';
 maxt=1010;
 maxn=110;
var
 opt:array[0..maxn,0..maxt] of longint;
 w,v:array[0..maxn] of longint;
 t,n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(t,n);
  for i:=1 to n do
   read(w[i],v[i]);
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  if x>y then max:=x
  else max:=y;
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for i:=1 to n do
   for j:=1 to t do
    if j-w[i]<0 then
     opt[i,j]:=opt[i-1,j]
    else opt[i,j]:=max(opt[i-1,j],opt[i-1,j-w[i]]+v[i]);
 end;
procedure print;
 begin
  writeln(opt[n,t]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.                     

//一维状态
program medic;
const
 fin='medic.in';
 fout='medic.out';
 maxt=1010;
 maxn=110;
var
 opt:array[0..maxt] of longint;
 w,v:array[0..maxn] of longint;
 ans,t,n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(t,n);
  for i:=1 to n do
   read(w[i],v[i]);
  close(input);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  opt[0]:=1;
  for i:=1 to n do
   for j:=t downto w[i] do
    if (opt[j-w[i]]>0) and (opt[j-w[i]]+v[i]>opt[j]) then
     opt[j]:=opt[j-w[i]]+v[i];
  ans:=-maxlongint;
  for i:=1 to t do
   if opt[i]>ans then ans:=opt[i];
 end;
procedure print;
 begin
  writeln(ans-1);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:背包问题-开心的金明

【问题描述】
    金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间他自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N 元钱就行”。今天一早金明就开始做预算,但是他想买的东西太多了,肯定会超过妈妈限定的N 元。于是,他把每件物品规定了一个重要度,分为5 等:用整数1~5 表示,第5 等最重要。他还从因特网上查到了每件物品的价格(都是整数元)。他希望在不超过N 元(可以等于N 元)的前提下,使每件物品的价格与重要度的乘积的总和最大。设第j 件物品的价格为v[j],重要度为w[j],共选中了k 件物品,编号依次为j1...jk,则所求的总和为:v[j1]*w[j1]+..+v[jk]*w[jk]请你帮助金明设计一个满足要求的购物单.
【输入文件】
    输入的第1 行,为两个正整数,用一个空格隔开: N  m(其中N(<30000)表示总钱数,m(<25)为希望购买物品的个数。)从第2 行到第m+1 行,第j 行给出了编号为j-1的物品的基本数据,每行有2 个非负整数 v p(其中v 表示该物品的价格(v≤10000),p 表示该物品的重要度(1~5))
【输出文件】
    输出只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<100000000)
【输入样例】
1000 5
800 2
400 5
300 5
400 3
200 2
【输出样例】
3900

【问题分析】

本题为典型的0/1背包,但需注意本题中的价值对应背包模型中的重量,其价值和重要度的成绩是背包模型中的价值。

program kaixing;
var
	i,j,m,n,w,v:longint;
	f:array[0..10000]of longint;
begin
	readln(m,n);
	for i:=1 to n do
	begin
		readln(w,v);
		for j:=m downto w do
		begin
			if f[j-w]+w*v>f[j] then f[j]:=f[j-w]+w*v;
		end;
	end;
	writeln(f[m]);
end.

例题:背包问题-金明的预算方案

【问题描述】
    金明今天很开心,家里购置的新房就要领钥匙了,新房里有一间金明自己专用的很宽敞的房间。更让他高兴的是,妈妈昨天对他说:“你的房间需要购买哪些物品,怎么布置,你说了算,只要不超过N元钱就行”。今天一早,金明就开始做预算了,他把想买的物品分为两类:主件与附件,附件是从属于某个主件的,下表就是一些主件与附件的例子:
主件      附件 
电脑      打印机,扫描仪 
书柜      图书 
书桌      台灯,文具 
工作椅  无 
    如果要买归类为附件的物品,必须先买该附件所属的主件。每个主件可以有0个、1个或2个附件。附件不再从属于其它附件。金明想买的东西很多,肯定会超过妈妈限定的N元。于是,他把每件物品规定了一个重要度,分为5等:用整数1~5表示,第5等最重要。他还从因特网上查到了每件物品的价格(都是10元的整数倍)。他希望在不超过N元(可以等于N元)的前提下,使每件物品的价格与重要度的乘积的总和最大。
    设第j件物品的价格为v[j],重要度为w[j],共选中了k件物品,编号依次为j1,j2,……,jk,则所求的总和为:v[j1]*w[j1]+v[j2]*w[j2]+ …+v[jk]*w[jk]。
请你帮助金明设计一个满足要求的购物单。
【输入文件】
    输入文件budget.in 的第1行,为两个正整数,用一个空格隔开:N  m (其中N(<32000)表示总钱数,m(<60)为希望购买物品的个数。)从第2行到第m+1行,第j行给出了编号为j-1的物品的基本数据,每行有3个非负整数: v  p  q(其中v表示该物品的价格(v<10000),p表示该物品的重要度(1~5),q表示该物品是主件还是附件。如果q=0,表示该物品为主件,如果q>0,表示该物品为附件,q是所属主件的编号)
【输出文件】
    输出文件budget.out只有一个正整数,为不超过总钱数的物品的价格与重要度乘积的总和的最大值(<200000)。
【输入样例】
1000 5
800 2 0
400 5 1
300 5 1
400 3 0
500 2 0
【输出样例】
2200

【问题分析】

例举出所有的主件附件购买方案,然后做01背包。如果附件数很多?购买方案为2^n。可以先对于每一个主件和附件集合做一次01背包,
然后再做一次背包!

本题为典型的背包问题,但较复杂的是其还存在附件和主件之分,由于附件最多两个,可先对主件与附件进行捆绑,因此做一个预处理——另设两个数组q1,q2来分别记录对应的第i个主件的附件,这样就不需对附件进行处理,而主件的花费W就有4种情况:

W1=v[i]                                     (只买主件)
W2=v[i]+v[q1[i]]                        (买主件和第一个附件)
W3=v[i]+v[q2[i]]                        (买主件和第二个附件)
W4=v[i]+v[q1[i]]+v[q2[i]]           (买主件和那两个附件)

设计一个状态opt[i]表示花i元钱可买到的物品的价格与重要度乘积之和最大值。边界条件是opt[0]=0,但是为了区分花i元钱是否可买到物品,需把初始条件opt[0]:=1;这样opt[i]>0说明花i元可以买到物品。这样就不难设计出这个状态的转移方程来:

opt[i]=max{opt[i],opt[i-wj]}       ((i-wj>0) and (opt[i-wj]>0)) (0<j<=4)

显然题目的解就是opt[1]到opt[n]中的一个最大值。但在输出时要注意将解减1。注意,价格是10的整数倍,所以读入数据时可以使n=n div 10,wi=wi div 10

program budget;
const
 fin='budget.in';
 fout='budget.out';
 maxn=3200;
 maxm=100;
var
 n,m,ans:longint;
 v,p,q1,q2,q:array[0..maxm] of longint;
 opt:array[0..maxn] of longint;
procedure init;
 var
  i,x:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(n,m);
  n:=n div 10;
  fillchar(q1,sizeof(q1),0);
  fillchar(q2,sizeof(q2),0);
  for i:=1 to m do
   begin
    readln(v[i],p[i],q[i]);
     v[i]:=v[i] div 10;
    q2[q[i]]:=q1[q[i]];
    q1[q[i]]:=i;
   end;
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  if x>y then exit(x);
  exit(y);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  opt[0]:=1;
  for j:=1 to m do
   for i:=n downto v[j] do
    if q[j]=0 then
     begin
      if (i-v[j]>=0) and (opt[i-v[j]]>0) then
       opt[i]:=max(opt[i],opt[i-v[j]]+v[j]*p[j]);
      if (i-v[j]-v[q1[j]]>=0) and (opt[i-v[j]-v[q1[j]]]>0) then
       opt[i]:=max(opt[i],opt[i-v[j]-v[q1[j]]]+v[j]*p[j]+v[q1[j]]*p[q1[j]]);
      if (i-v[j]-v[q2[j]]>=0) and (opt[i-v[j]-v[q2[j]]]>0) then
       opt[i]:=max(opt[i],opt[i-v[j]-v[q2[j]]]+v[j]*p[j]+v[q2[j]]*p[q2[j]]);
if (i-v[j]-v[q1[j]]-v[q2[j]]>=0) and (opt[i-v[j]-v[q1[j]]-v[q2[j]]]>0) then
opt[i]:=max(opt[i],opt[i-v[j]-v[q1[j]]-v[q2[j]]]+v[j]*p[j]+v[q1[j]]*p[q1[j]]+v[q2[j]]*p[q2[j]]);
      ans:=max(ans,opt[i]);
     end;
 end;
procedure print;
 begin
  writeln((ans-1)*10);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:背包问题-Money Systems

【问题描述】
母牛们创建了他们自己的政府,而且选择了建立自己的货币系统。传统地,一个货币系统是由1,5,10,20或25,50和100的单位面值组成的。母牛想知道有多少种不同的方法来用货币系统中的货币来构造一个确定的数值。举例来说, 使用一个货币系统 {1,2,5,10,...}产生18单位面值的一些可能的方法是:18x1, 9x2, 8x2+2x1, 3x5+2+1,等等其它。写一个程序来计算有多少种方法用给定的货币系统来构造一定数量的面值。保证总数将会适合long long (C/C++) 和 Int64 (Free Pascal)。
【输入文件】
货币系统中货币的种类数目是 V (1<= V<=25)。要构造的数量钱是 N (1<= N<=10,000)。
第 1 行:两整数,V和N
第 2 行:可用的货币,V个整数
【输出文件】
单独的一行包含那个可能的构造的方案数。
【输入样例】
3 10
1 2 5
【输出样例】
10
【提交链接】
http://train.usaco.org/

【问题分析】

将需构造的钱数值N看作背包,问题变成0/1背包的简化版,但本题不是判断N是否可构成,而是求构成N的方案,而且这里的面值是可以重复利用的,即物品有无限多。对于第一个问题,只要把原来Boolean型的状态改为Iint64,在递推过程中累加方案数即可;对于第二个问题,基本模型中为了避免重复,在内层循环枚举背包载重时采用倒循环,现在只要正向循环即可。复杂度与原模型相同。

{
ID:hhzhaojia2
PROG:money
LANG:PASCAL
}
program money;
const
 fin='money.in';
 fout='money.out';
 maxv=100;
 maxn=10010;
var
 a:array[0..maxv] of longint;
 opt:array[0..maxn] of int64;
 v,n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(v,n);
  for i:= 1 to v do
   read(a[i]);
  close(input);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  opt[0]:=1;
  for i:=1 to v do
   for j:=a[i] to n do
     inc(opt[j],opt[j-a[i]]);
 end;
procedure print;
 begin
  writeln(opt[n]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:背包问题-挖地雷问题

【问题描述】
在一个地图上有N个地窖(N<=20),每个地窖中埋有一定数量的地雷,同时给出地窖之间的连接路径。某人可以从任一处开始挖地雷,然后可以沿着其连接的地窖往下挖(仅能选择一条路径),当无连接时挖地雷工作结束。设计一个挖地雷的方案,使某人能挖到最多的地雷。
【输入文件】
N:(表示地窖的个数)
W1,W2,W3,……WN(表示每个地窖中埋藏的地雷数量)
A12………………A1N(地窖之间连接路径,其中Aij=1表示地窖i与地窖j之间有通路,不通则Aij=0)
      A23…………..A2N        
                   ……..……..
                           AN-1N
【输出文件】
K1->K2->……….KV(挖地雷的顺序)
MAX(挖地雷的数量)
【输入样例】
5
10,8,4,7,6
1  1  1  0
    0  0  0
        1  1
            1
【输出样例】
 1->3->4->5
 max=27
【Hint】
题目中的路径是有向无环路。

【问题分析】

首先,考虑贪心——以一点出发,找与其连接的地窖中地雷数最多的一个。但很容易想到反例:

5
1 2 1 1 100
1 1 0 0
   0 1 0
      0 1
         0

按照贪心答案是4,但实际上答案是102。于是就不得不放弃贪心的想法。但能从贪心策略中得到启示:从一个顶点出发要选择向一个与他相连且以该点出发可以挖到较多雷的点走,即,如果一个顶点连同N个分量,显然要选一个较大的作为解,这个定义是满足最优化原理的。且满足无后效性:因为图是有向的,所以以与该顶点相连的点再往下走的路线中不包括该点。也就是说图是一个AOV网(有向无环图)。既然满足最优化原理,且无后效性,就可以用动态规划求解:此题的阶段就是拓扑序列,但由于输入是倒三角形,所以无需求拓扑序列,只要从N倒着求解即可。

设计状态opt[i]表示以i点出发可以挖到最多的雷的个数。状态转移方程:opt[i]=max{opt[j]}+w[i] (g[i,j]=1) (g存图,w[i]存第i个地窖中的雷的个数)。

【时间复杂度】

状态数O(n)*转移代价O(n)=O(n^2)

本题还要求出路径,可用一个辅助数组path来记录,path[i]表示从第i个出发走到的下一个点的编号。求解完只要按path记录的路径输出即可。

program P3;
const
 fin='P3.in';
 fout='P3.out';
 maxn=200;
var
 g:array[0..maxn,0..maxn] of longint;
 n,ans:longint;
 w,opt,path:array[0..maxn] of longint;
procedure init;
 var
  i,j:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  fillchar(g,sizeof(g),0);
  for i:=1 to n do
   read(w[i]);
  for i:=1 to n do
   for j:=i+1 to n do
    read(g[i,j]);
  close(input);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  fillchar(path,sizeof(path),0);
  for i:=n downto 1 do
   begin
    for j:=i+1 to n do
     if (g[i,j]=1)  and (opt[j]>opt[i]) then
      begin
       opt[i]:=opt[j];
       path[i]:=j;
      end;
    inc(opt[i],w[i]);
   end;
  ans:=1;
  for i:=2 to n do
   if opt[i]>opt[ans] then  ans:=i;
 end;
procedure print;
 var
  i:longint;
 begin
  write(ans);
  i:=path[ans];
  while i>0 do
   begin
    write('-->',i);
    i:=path[i];
   end;
  writeln;
  writeln('max=',opt[ans]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:拦截导弹

【问题描述-poj1887】
某国为了防御敌国的导弹袭击,发展出一种导弹拦截系统。但是这种导弹拦截系统有一个缺陷:虽然它的第一发炮弹能够到达任意的高度,但是以后每一发炮弹都不能高于前一发的高度。某天,雷达捕捉到敌国的导弹来袭。由于该系统还在试用阶段,所以只有一套系统,因此有可能不能拦截所有的导弹。输入导弹依次飞来的高度,计算这套系统最多能拦截多少导弹。

【问题分析】
状态的表示:f[i]表示当第i个导弹必须拦截时,前i个导弹最多能拦截掉多少。
状态转移方程:
f[i]=max(f[j])+1(0<=j<i 且a[j]>a[i])

例题:二维状态-数塔问题

【问题描述】
考虑如下数字金字塔,写一个程序来计算从最高点开始在底部任意处结束的路径经过数字之和的最大值。每一步可以走到左下方的点也可以到达右下方的点。
        7
      3   8
    8   1   0
  2   7   4   4
4   5   2   6   5
在上面的样例中,从7到3到8到7到5的路径产生了最大和:30。
【输入文件】
第一行包含R(1<=R<=1000),表示行的数目。后面每行为这个数字金字塔特定行包含的整数。所有整数是非负的且不大于100。
【输出文件】
单独一行,即可能得到的最大和。
【输入样例】


3 8 
8 1 0 
2 7 4 4 
4 5 2 6 5  
【输出样例】
30

【问题分析】

本题为最优化问题,如果用枚举法(暴力思想),在数塔层数稍大的情况下(如31),则需要列举出的路径条数将是一个非常大的规模:2^30=1024^3>10^9=10亿。

从顶点出发后选择向左还是右走取决于走哪边能取到最大值,只要左右两条路上的最大值求出来了才能作出决策,同样下一层的走向又得取决于再下一层的最大值是否已经求出才能决策。

 

数字金字塔的输入样例是下三角,可用一个二维数组a储存,并可用(i,j)描述一个数字在金字塔中的位置。对于中间的一个点来说,想经过它则必须经过它的上方或左上(针对变化后的三角形)。也就是说,经过这个点的数字和最大,等于经过上方或左上所得的“最大和”中一个更大的加上这个点中的数字。显然这个定义满足最优子结构。这样阶段很明显就是金字塔的层,设计一个二维状态opt[i,j]表示走到第i行第j列时经过的数字的最大和。决策就是opt[i-1,j]或opt[i-1,j-1]中一个更大的加上(i,j)点的数字,同时,对于一个点只考虑上面或左上即前一阶段,满足无后效性。状态转移方程为:

opt[i,j]=\left\{\begin{array}{lr}opt[i-1,j]+a[i,j]\:(j=1) & \\ opt[i-1,j-1]+a[i,j]\:(j=i)& \\ max\{opt[i-1,j],opt[i-1,j-1]\}+a[i,j]\:(1<j<i) & \end{array} \right.

具体可将opt[i,j]左右边界定义大点,初始opt[i,j]=0,在j=1时opt[i-1,j-1]=0,opt[i-1,j]>=0,所以可写成opt[i,j]=max{opt[i-1,j],opt[i-1,j-1]}+a[i,j],同理,j=i时,方程也可写成上面那样,所以方程综合为:opt[i,j]=max{opt[i-1,j],opt[i-1,j-1]}+a[i,j](0<j<=i)。显然答案是走到底后的一个最大值,即:ans=max{opt[n,i]} (1<=i<=n),其实从上往下走和从下往上走结果是一样的,但是如果从下往上走,结果就是opt[1,1],省下求最大值了,所以方程进一不改动为:opt[i,j]=max{opt[i+1,j],opt[i+1,j+1]}+a[i,j](0<j<=i)。

【时间复杂度】

状态数O(N^2)*转移代价O(1)=O(N^2)

program numtri;
const
 fin='numtri.in';
 fout='numtri.out';
 maxn=1010;
var
 a,opt:array[0..maxn,0..maxn] of longint;
 n:longint;
procedure init;
 var
  i,j:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  for i:=1 to n do
   for j:=1 to i do
    read(a[i,j]);
  close(input);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for i:=n downto 1 do
   for j:=1 to n do
    if opt[i+1,j]>opt[i+1,j+1] then
     opt[i,j]:=opt[i+1,j]+a[i,j]
    else opt[i,j]:=opt[i+1,j+1]+a[i,j];
 end;
procedure print;
 begin
  writeln(opt[1,1]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:街道问题

【问题描述】
如图所示的矩形图中找到一条从左下角到右上角的最短路径,图中数字表示边的长度。只能向右或向上走。


【输入文件】
第一行两个数,N,M 矩形的点有N行M列(0<N,M<1000)。
接下来N行每行M-1个数描述横向边的长度。
接下来N-1行每行M个数描述纵向边的长度。
边的长度小于10。
【输出文件】
最短路径长度。
【输入样例】
4 5
3 7 4 8
4 6 3 5
3 6 3 5
5 4 6 2
7 6 3 5 3
2 8 5 9 4
8 7 4 3 7
【输出样例】
28

【问题分析】

因为只能向右或向上走,将图进行变换后,阶段应该是:

 

问题转换为数塔问题,只是数塔问题的数在点上而街道问题的数在边上,但解题思路一致。设计一个二维状态opt[i,j]表示走到(i,j)的最短路径,显然这个路径只可能是左边或上边走来的,所以决策就是这两个方向上加上经过的边的和中一个较短的路。状态转移方程为:

opt[i,j]=\left\{\begin{array}{lr}opt[i+1,j]+z[i,j]\:(j=1) & \\ opt[i,j-1]+h[i,j]\:(i=n)& \\ min\{opt[i+1,j]+z[i,j],opt[i,j-1]+h[i,j]\}\:(0<i\leq n,0<j\leq m) & \end{array} \right.

类似于数塔问题预处理,初始化opt的值为很大的数,保证解不会超过其值,但要注意不要太的,太大可能有溢出问题,设opt[0,0]=0,方程整理为:opt[i,j]=min{opt[i+1,j]+z[i,j],opt[i,j-1]+h[i,j]}。

【时间复杂度】

状态数O(N^2)*转移代价O(1)=O(N^2)

program way;
const
 fin='way.in';
 fout='way.out';
 maxn=1010;
var
 h,z,opt:array[0..maxn,0..maxn] of longint;
 n,m:longint;
procedure init;
 var
  i,j:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n,m);
  for i:=1 to n do
   for j:=2 to m do
    read(h[i,j]);
  for i:=1 to n-1  do
   for j:=1 to m do
    read(z[i,j]);
  close(input);
 end;
function min(x,y:longint):longint;
 begin
  min:=y;
  if x<y then min:=x;
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),$7F);
  opt[n,0]:=0;
  for i:=n downto 1 do
   for j:=1 to m do
    opt[i,j]:=min(opt[i+1,j]+z[i,j],opt[i,j-1]+h[i,j]);
 end;
procedure print;
 begin
  writeln(opt[1,m]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:最长上升子序列(LIS)问题

【问题描述】

给出一个由n个数组成的序列X[1,……,n],找出它的最长单调上升子序列。即求最大的m和a1,a2,……,am,使得a1<a2<……<am且X[a1]<X[a2]<……<X[am]。

【问题分析】

如果前i-1个数中用到X[j](X[j]<X[i])构成了一个的最长的上升序列,加上第i个数X[i]就是前i个数中用到i的最长的序列了。从上面的分析可以看出这样划分问题满足最优子结构,那满足无后效性么?显然对于第i个数时只考虑前i-1个数,显然满足无后效性,可以用动态规划解。状态转移方程为:

f(n)=max{ f(k)+1 | 0\leqk<n,X[k]<X[n] }

【时间复杂度】

O(N^2)

For (i=1;i<=n;i++)
    For (j=1;j<i;j++)
        If (a[j]<a[i])
            F[i] = max(f[i],f[j]+1);

例题:二维状态-最长公共子序列(LCS)问题

【问题描述】
    一个给定序列的子序列是在该序列中删去若干元素后得到的序列。确切地说,若给定序列X=<x1,x2,…, xm>,则另一序列Z=<z1,z2,…, zk>是X的子序列是指存在一个严格递增的下标序列<i1,i2,…,ik>,使得对于所有j=1,2,…,k有Xij=Zj
    例如,序列Z=<B,C,D,B>是序列X=<A,B,C,B,D,A,B>的子序列,相应的递增下标序列为<2,3,5,7>。给定两个序列X和Y,当另一序列Z既是X的子序列又是Y的子序列时,称Z是序列X和Y的公共子序列。例如,若X=< A,B,C,B,D,A,B>和Y=<B,D,C,A,B,A>,则序列<B,C,A>是X和Y的一个公共子序列,序列<B,C,B,A>也是X和Y的一个公共子序列。而且,后者是X和Y的一个最长公共子序列,因为X和Y没有长度大于4的公共子序列。
    给定两个序列X=<x1,x2,…,xm>和Y=<y1,y2,…,yn>,要求找出X和Y的一个最长公共子序列(不要求连续)。
【输入文件】
    输入文件共有两行,每行为一个由大写字母构成的长度不超过200的字符串,表示序列X和Y。
【输出文件】
    输出文件第一行为一个非负整数,表示所求得的最长公共子序列的长度,若不存在公共子序列,则输出文件仅有一行,输出一个整数0,否则在输出文件的第二行输出所求得的最长公共子序列(由大写字母组成的字符串表示)。
【输入样例】
ABCBDAB 
BDCBA 
【输出样例】

BCBA 
【提交链接】
http://mail.bashu.cn:8080/JudgeOnline 

【问题分析】

最长公共子序列也称作最长公共子串(不要求连续),英文缩写为LCS(Longest Common Subsequence)。其定义是,一个序列S,如果分别是两个或多个已知序列的子序列,且是所有符合此条件序列中最长的,则S称为已知序列的最长公共子序列。

该题也是经典的动态规划问题。首先,划分子问题:对于街道问题和数塔问题,涉及走向的,子问题考虑上一步状态,但本题不涉及走向,阶段很不明显,但既然是求公共子序列,也就有第一个序列的第i个字符和第二个序列的第j个字符相等的情况,可枚第一个序列X的字符和第二个序列Y的字符。

一维状态无法表示状态空间,使用二维状态F(i,j)。假设此时子序列起点为1,枚举到判断X[i]与Y[j]:如果X[i]=Y[j],序列X前i个字符的子序列和序列Y前j个字符的子序列的最长公共子序列就是序列X前i-1个字符和序列Y前j-1个字符的子序列中最长的公共子序列加上X[i]或Y[j];如果X[i]\neqY[j],也就是说序列X前i个字符的子序列和序列Y前j个字符的子序列的公共子序列中X[i]和Y[j]不同时出现。也就是说序列X前i个字符的子序列和序列Y前j个字符的子序列的公共子序列是:序列X前i个字符的子序列和序列Y前j-1个字符的子序列的公共子序列中较长者,或序列X前i-1个字符的子序列和序列Y前j个字符的子序列的公共子序列中较长者。

设计一个状态opt[i,j]表示起点为1,序列X长度为i和序列Y长度为j的子序列的最长公共子序列。依此可得到状态转移方程:

opt[i,j]=\left\{\begin{array}{lr}opt[i-1,j-1]+x[i]\:(x[i]=y[j]) & \\ opt[i-1,j]+x[i]\:(length(opt[i-1,j])\geq length(opt[i,j-1]))& \\ opt[i,j-1]+y[j]\:(length(opt[i-1,j])<length(opt[i,j-1])) & \end{array} \right.

【时间复杂度】

状态数O(N^2)*转移代价O(1)=O(N^2)

program LCS;
const
 fin='LCS.in';
 fout='LCS.out';
 maxn=300;
var
 s1,s2:string;
 opt:array[0..maxn,0..maxn] of string;
 L1,L2:longint;
procedure init;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(s1);
  readln(s2);
  L1:=length(s1);
  L2:=length(s2);
  close(input);
 end;
procedure main;
 var
  i,j:longint;
 begin
  for i:=1 to L1 do
   for j:=1 to L2 do
    opt[i,j]:='';
  for i:=1 to L1 do
   for j:=1 to L2 do
    if s1[i]=s2[j] then
     opt[i,j]:=opt[i-1,j-1]+s1[i]
    else if length(opt[i-1,j])>=length(opt[i,j-1]) then
     opt[i,j]:=opt[i-1,j]
    else opt[i,j]:=opt[i,j-1];
 end;
procedure print;
 begin
  writeln(length(opt[L1,L2]));
  write(opt[L1,L2]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:最大子段和问题

【问题描述】

给定长度为n的整数序列,a[1...n], 求[1,n]某个子区间[i , j]使得a[i]+…+a[j]和最大或者求出最大的这个和。
例如(-2,11,-4,13,-5,2)的最大子段和为20,所求子区间为[2,4]。

【问题分析】

已知包含第n个数的最大子段,那么包含第n+1个数的最大子段有两种情况:1)包含第n个数的最大子段;2)不包含。

设数组为a[k],1≤k≤n,最大子段和X被定义为:X=\max_{1\leq i<j\leq n}\{\sum_{k=i}^{j}a[k] \},不妨设:b[j]=\max_{1\leq j\leq n}\{\sum_{k=m}^{j}a[k] \},其中m是可变的。注意:a[j]必须是b[j]这个最大局部受限子段和所对应子段的最右端,根据 b[j]和X 的定义,不难发现:X=\max_{1\leq j\leq n}b[j]。另一方面,根据b[j]的定义,可以看出:1)当b[j-1]>0 时,无论a[j]为何值,b[j]=b[j-1]+a[j];2)当 b[j-1]≤0 时,无论a[j]为何值,b[j]=a[j]。所以有:b[j]=\max_{1\leq j\leq n}\{b[j-1]+a[j],a[j]\}

F(n)=max{F(n-1)+a[n],a[n]}

其中:b[1]=a[1],b[2]=b[1]+a[2],b[3]=a[3],b[4]=b[3]+a[4];因此,对于数组a而言,最大子段和为b[4],即X=12。

【复杂度】

时间复杂度=O(n)

typedef int elem_t; //定义元素类型
elem_t maxsum(int n,elem_t* list){
	elem_t ret,sum=0;
	int i;
	for (ret=list[i=0];i<n;i++)
		sum=(sum>0?sum:0)+list[i],ret=(sum>ret?sum:ret);
	return ret;
}
//返回最大子段和对应的子段位置(maxsum=list[start]+...+list[end])
elem_t maxsum(int n,elem_t* list,int& start,int& end){
	elem_t ret,sum=0;
	int s,i;
	for (ret=list[start=end=s=i=0];i<n;i++,s=(sum>0?s:i))
		if ((sum=(sum>0?sum:0)+list[i])>ret)
			ret=sum,start=s,end=i;
	return ret;
}

#include<iostream>
using namespace std;
int a[n];
int MaxSun(){
    int sum = 0, b = 0;
    int i;
    for(i = 0; i <= n; i++)
    {
       if(b > 0) b += a[i];
       else b = a[i];
       if(b > sum) sum = b;
    }
    return sum;
}

例题:最大子矩阵和问题

【问题描述】

给定一个n*n(0<n<=300)的矩阵,请找到此矩阵的一个子矩阵,并且此子矩阵的各个元素的和最大,输出这个最大的值。

【输入文件】
0 -2 -7 0
9 2 -6 2
-4 1 -4 1
-1 8 0 -2

【输出文件】

15

【解释】
其中左上角的子矩阵:
9 2
-4 1
-1 8
此子矩阵的值为9+2+(-4)+1+(-1)+8=15

【问题分析】

若采用枚举,复杂度为O(N^4)=300^4=8.1*10^9,会超时,需简化问题,采用降维。如果是1d的问题如何解决?

最大子矩阵和其实是最大子段和问题的二维推广,其基本思路为:若始行i1与末行i2已给定,则求以i1起始以i2结束的最大子矩阵之和,即等于一个一维的最大子段和问题,只不过这里的数组a中元素a[j]是第j列里从第i1行加到第i2行的所有元素之和。令t[i1,i2]表示这个行从i1到i2的最大子矩阵和,则求全矩阵的最大子矩阵之和的问题就等于在1\leqi1\leqi2\leqm的范围中使t[i1,i2]最大化。
显然上述算法的时间复杂度为O(m^2*n),然而,容易看出,整个问题的解决本质上还是一个一维最大子段和的问题,而在另一个维度(行)上,则还是枚举所有的1\leqi1\leqi2\leqm,用打擂的方法比较出最大者。也就是说,此方法仍然只是在列这个维度上用到了动态规划。

【复杂度】

时间复杂度=O(n^3)

#include"stdio.h" 
int main(int argc, char* argv[]) { 
    int a[4][5]={{-1,-2,3,4,-8},{2,-1,3,-9,6},{5,8,4,4,-1},{-7,8,2,-5,0}}; 
    int m=4,n=5; 
    int i1,i2,j1,j,r1,r2,c1,c2; 
    int maxsum=0,tsum,csum[5]; 
    for(i1=0;i1 <n;i1++) csum[i1]=0;//csum[k]表示在第k列从第i1到第i2行的元素之和 
    bool reverse=false;  //表示i2增减的方向 
    //这里为了可以每次只在csum[]上加或减一行元素,采用i2来回运动的方式.对复杂度的数量级并不能减小,因此意义不大 
    for(i1=0,i2=-2;i1<m;i1++){ 
        if(!reverse) i2++; 
        while((reverse && --i2>=i1)||((!reverse) && ++i2<m)){ 
            for(j=0;j<n;j++){ 
               if(reverse){ 
                  if(i2<m-1) csum[j]-=a[i2+1][j]; 
                  else csum[j]-=a[i1-1][j]; 
                } 
                else csum[j]+=a[i2][j]; 
            }//更新csum[] 
            tsum=0; 
            j1=0; 
            for(j=0;j<n;j++){ 
                if((tsum+=csum[j])<0){ 
                    tsum=0; 
                    j1=j+1; 
                } 
                else if (tsum>maxsum){ 
                    c1=j1;c2=j;r1=i1;r2=i2;maxsum=tsum; 
                } 
            }//动态归划求解最大的t(i1,i2) 
       } 
       reverse=(!reverse); 
    } 
    printf("r1=%d,c1=%d,r2=%d,c2=%d.MaxSubSum=%d\n",r1,c1,r2,c2,maxsum); 
    return 0; 
} 

#define MAXN 100
typedef int elem_t;
elem_t maxsum(int m,int n,elem_t mat[][MAXN]){
	elem_t matsum[MAXN][MAXN+1],ret,sum;
	int i,j,k;
	for (i=0;i<m;i++)
		for (matsum[i][j=0]=0;j<n;j++)
			matsum[i][j+1]=matsum[i][j]+mat[i][j];
	for (ret=mat[0][j=0];j<n;j++)
		for (k=j;k<n;k++)
			for (sum=0,i=0;i<m;i++)
				sum=(sum>0?sum:0)+matsum[i][k+1]-matsum[i][j],ret=(sum>ret?sum:ret);
	return ret;
}
elem_t maxsum(int m,int n,elem_t mat[][MAXN],int& s1,int& s2,int& e1,int& e2){
	elem_t matsum[MAXN][MAXN+1],ret,sum;
	int i,j,k,s;
	for (i=0;i<m;i++)
		for (matsum[i][j=0]=0;j<n;j++)
			matsum[i][j+1]=matsum[i][j]+mat[i][j];
	for (ret=mat[s1=e1=0][s2=e2=j=0];j<n;j++)
		for (k=j;k<n;k++)
			for (sum=0,s=i=0;i<m;i++,s=(sum>0?s:i))
				if ((sum=(sum>0?sum:0)+matsum[i][k+1]-matsum[i][j])>ret)
					ret=sum,s1=s,s2=i,e1=j,e2=k;
	return ret;
}

#include<iostream>
using namespace std;
const int MAX = 10; //行列个数
int num[MAX][MAX];
int n;//行列实际个数
int colNum[MAX];//暂时存储每列的元素之和
int MaxSum(){
    int sum = 0, b = 0;
    int i;
    for(i = 0; i < n; i++){
       if(b > 0) b += colNum[i];
       else b = colNum[i];
       if(b > sum) sum = b;
    }
    return sum;
}
int findMax(){
    int i,j,k,m;
    int ans = 0;
    for(i = 0; i < n; i++){
       for(k = 0; k < n; k++)
            colNum[k] = num[i][k];
       for(j = i+1; j < n; j++){
            for(k = 0; k < n; k++)
                 colNum[k] += num[j][k];
            int temp = MaxSum();
            if(temp > ans) ans = temp;
       }
    }
    return ans;
}
int main(){
    cin >> n;
    for(int i = 0; i < n; i++)
       for(int j = 0; j < n; j++)
          cin >> num[i][j];
    cout << findMax() << endl;
    return 0;
}

例题:回文词

【问题描述】
回文词是一种对称的字符串——也就是说,一个回文词,从左到右读和从右到左读得到的结果是一样的。任意给定一个字符串,通过插入若干字符,都可以变成一个回文词。你的任务是写一个程序,求出将给定字符串变成回文词所需插入的最少字符数。比如字符串“Ab3bd”,在插入两个字符后可以变成一个回文词(“dAb3bAd”或“Adb3bdA”)。然而,插入两个以下的字符无法使它变成一个回文词。
【输入文件】
第一行包含一个整数N,表示给定字符串的长度,3<=N<=5000
第二行是一个长度为N的字符串,字符串由大小写字母和数字构成。
【输出文件】
一个整数,表示需要插入的最少字符数。
【输入样例】
5
Ab3bd
【输出样例】
2

【问题分析】

所谓回文词,其实就是从中间断开,将后面翻转后与前面部分一样(注意奇数和偶数有区别)。例:

回文词:AB3BA
断开:AB BA(奇数个时去掉中间字符)
翻转:AB AB

 本题求最少填几个字符可使一个字符串变成回文词,也就是说从任意点截断,再翻转后面部分后。两个序列有相同的部分不用添字符,不一样的部分添上字符就可以了。例:

回文词:Ab3bd
截断:Ab bd
翻转:Ab db

b在两个序列里都有,再在第二个里添A并在第一个里添d即可,即:Adb Adb。这样添两个就可以了,显然从别的地方截断添的个数要比这样多。这样就把原问题抽象为最长公共子序列问题了。枚举截断点,把原串截断,翻转。求最长公共子序列。答案就是len-(ans*2),len是翻转后两个序列的长度和,ans是最长公共子序列的长度。

其实这样求解还是存在重复工作,答案既然在最后求解ans还要乘2,那么在先前计算时直接把原串翻转为第二个序列,和原序列求最长公共子序列即可,这样最后求解就不用乘2,也不用枚举截断点了,例:

原串:Ab3bd
翻转:db3bA
最长公共子序列b3b
添加2个字符

如何理解这个优化呢?其实翻转了序列后,其字符的先后顺序就变了,求解最长公共子序列中得到的解,是唯一的,也就是说这个序列的顺序是唯一的,如果在翻转后的序列和原串能得到相同的序列,那么这个序列在两个串中字符间的顺序是横定的,就已经满足了回文词的定义(正着读和反着读一样)。所以这个优化是正确的。

注意:这个问题的数据规模很大,空间复杂度较高,达O(N^2),所以要用到滚动数组。

program P1327;
const
 maxn=5002;
var
 a,b:ansistring;
 opt:array[0..1,0..maxn] of longint;
 n,ans:longint;
function max(x,y:longint):longint;
 begin
  if x>y then exit(x);
  max:=y;
 end;
procedure main;
 var
  i,x,j,k0,k1:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  readln(n);
  readln(a);
  b:='';
  for i:=n downto 1 do
   b:=b+a[i];
  k0:=0;
  k1:=1;
  for i:=1 to n do
   begin
    fillchar(opt[k1],sizeof(opt[k1]),0);
    for j:=1 to n do
     begin
      opt[k1,j]:=max(opt[k0,j],opt[k1,j-1]);
      if a[i]=b[j] then
       opt[k1,j]:=max(opt[k1,j],opt[k0,j-1]+1);
     end;
    x:=k0;
    k0:=k1;
    k1:=x;
   end;
  writeln(n-opt[k0,n]);
 end;
begin
 main;
end.

【算法改进】

从原问题出发,找其子问题。类似于最长公共子序列问题,涉及序列的问题,一般先考虑其子序列,即更短的序列。然后可以利用边界条件法,显然对于本题,单独的字符就是边界,而且单独的字符就是回文词,添加0个字符就可以了。再考虑两个字符组成的序列,只要看两字符是否相同即可:如果相同,那就已经是回文词,添加0个字符;如果不相同,就在它的左边或右边添一个字符,让另外一个当对称轴。如果是三个字符的字符串S呢?如果S[1]=S[3]那么它就是回文词了,如果S[1]\neqS[3],那么就在前面添S[3]或后面添S[1],剩下的就还要考虑S[1]S[2]或S[2]S[3]这两个序列了。

通过前面的分析,很容易想到这样的算法——对于一个序列S只要看它的左右端的字符是否相同:如果相同那么就看除掉两端字符的新串要添的字符个数了;如果不同,就在它左面添上右端的字符然后考虑去掉新序列两端的字符后的串要添的字符个数,或者在右面添上左端的字符再考虑去掉新序列两端字符后得到的新串要添的字符个数。

设计一个二维状态opt[L,i]表示长度是L+1,起点是i的序列变成回文词要添的字符的个数。阶段就是字符长度,决策分为S[i]和S[i+L]是否相等两种。状态转移方程为:

opt[L,i]=\left\{\begin{array}{lr}min(opt[L-1,i]+1,opt[L-1,i+1]+1)\:(s[i]\neq s[i+L]) & \\ min(opt[L-1,i]+1,opt[L-1,i+1]+1,opt[L-2,i+1])\:(s[i]=s[i+L]) & \end{array} \right.

【复杂度】

空间复杂度=状态数O(N^2)

时间复杂度=状态数O(N^2)*转移代价O(1)=O(N^2)

由于空间复杂度较高,仍然要用滚动数组。

program P1327;
const
 maxn=5002;
var
 a:array[0..maxn] of char;
 opt:array[0..2,0..maxn] of longint;
 n,ans:longint;
function min(x,y:longint):longint;
 begin
  min:=y;
  if x<y then min:=x;
 end;
procedure main;
 var
  i,L,j,k0,k1,k2:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  readln(n);
  for i:=1 to n do
   read(a[i]);
  k0:=0;
  k1:=1;
  k2:=2;
  for L:=1 to n-1 do
   begin
    for i:=1 to n-L do
     begin
      opt[k2,i]:=min(opt[k1,i],opt[k1,i+1])+1;
      if a[i]=a[i+L] then
       opt[k2,i]:=min(opt[k2,i],opt[k0,i+1]);
     end;
    j:=k0;
    k0:=k1;
    k1:=k2;
    k2:=j;
   end;
  writeln(opt[k1,1]);
 end;
begin
 main;
end.

例题:调整队形

【问题描述】
学校艺术节上,规定合唱队要参加比赛,各个队员的衣服颜色不能很混乱:合唱队员应排成一横排,且衣服颜色必须是左右对称的。例如:“红蓝绿蓝红”或“红蓝绿绿蓝红”都是符合的,而“红蓝绿红”或“蓝绿蓝红”就不符合要求。合唱队人数自然很多,仅现有的同学就可能会有3000个。老师希望将合唱队调整得符合要求,但想要调整尽量少,减少麻烦。以下任一动作认为是一次调整:
1、在队伍左或右边加一个人(衣服颜色依要求而定);
2、在队伍中任两个人中间插入一个人(衣服颜色依要求而定);
3、剔掉一个人;
4、让一个人换衣服颜色;
老师想知道就目前的队形最少的调整次数是多少,请你编一个程序来回答他。因为加入合唱队很热门,你可以认为人数是无限的,即随时想加一个人都能找到人。同时衣服颜色也是任意的。
【输入文件】
第一行是一个整数n(1≤n≤3000)。
第二行是n个整数,从左到右分别表示现有的每个队员衣服的颜色号,都是1到3000的整数。
【输出文件】
一个数,即对于输入队列,要调整得符合要求,最少的调整次数。
【输入样例】

1 2 2 4 3 
【输出样例】
2
【提交链接】
http://oi.tju.edu.cn/problem/submit/1006/

【问题分析】

本题类似于回文词,但决策比回文词多些,不仅可以插入一个人(词),还可以剔人,还可以换服装,不过其实剔人和插入是等价的,也就是说本题比回文词只多了一个条件——换服装,即序列中的元素不固定,不能用上述回文词第一种方法,只能用第二种方法:阶段是序列的长度,状态是opt[i,j]表示[i,j]这段区间内要变成回文所需要的最少的调整次数,决策比回文词多一个即如果左右两端不一样还可以通过换服装这种方式只花费一次的代价调整好。状态转移方程为:

opt[i,j]=\left\{\begin{array}{lr}min(opt[i,j-1]+1,opt[i+1,j]+1,opt[i+1,j-1]+1)\:(a[i]\neq a[j],1\leq i<j\leq n) & \\ min(opt[i,j-1]+1,opt[i+1,j]+1,opt[i+1,j-1])\:(a[i]=a[j],1\leq i<j\leq n) & \end{array} \right.

边界条件:opt[i,i]=0 (1<=i<=n)

【时间复杂度】

状态数O(N^2)*转移代价O(1)=总复杂度O(N^2)

program queue;
const
 fin='queue.in';
 fout='queue.out';
 maxn=3000;
var
 a:array[0..maxn] of longint;
 opt:array[0..maxn,0..maxn] of longint;
 n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(n);
  for i:=1 to n do
   read(a[i]);
 end;
procedure main;
 var
  i,j,L:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for L:=1 to n-1 do
   for i:=1 to n-L do
    begin
     j:=i+L;
     if opt[i+1,j]+1<opt[i,j-1]+1 then
      opt[i,j]:=opt[i+1,j]+1
     else opt[i,j]:=opt[i,j-1]+1;
     if a[i]=a[j] then
      begin
       if opt[i+1,j-1]<opt[i,j] then
        opt[i,j]:=opt[i+1,j-1]
      end
     else begin
           if opt[i+1,j-1]+1<opt[i,j] then
            opt[i,j]:=opt[i+1,j-1]+1;
          end;
    end;
 end;
procedure print;
 begin
  writeln(opt[1,n]);
  close(input);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:背包问题拓展-找啊找啊找GF

【问题描述】
@看中了n个MM,编号为1到n,请i号MM吃饭要花rmb[i]块大洋,请i号MM吃饭试图让她当GF的行为(不妨称作泡MM)要耗费rp[i]的人品。而对于每一个MM来说@都有一个对应的搞定她的时间,对于i号MM来说,需要time[i]。@保证自己有足够的魅力用time[i]的时间搞定i号MM。@希望搞定尽量多的MM当自己的GF,但他不希望为此花费太多的时间,所以他希望在保证搞定MM数量最多的情况下花费的总时间最少。@现在有m块大洋,并攒到了r的人品,他凭借这些大洋和人品可以泡到一些MM。他想知道,自己泡到最多的MM花费的最少时间是多少,当然@在一个时刻只能去泡一个MM。
【输入文件】
输入的第一行是n,表示@看中的MM数量
接下来有n行,依次表示编号为1, 2, 3, ..., n的一个MM的信息,每行表示一个MM的信息,有三个整数:rmb,rp和time
最后一行有两个整数,分别为m和r
【输出文件】
输出一行,为一个整数,表示@在保证MM数量最多的情况下花费的最少总时间
【输入样例】
4
1 2 5
2 1 6
2 2 2
2 2 3
5 5
【输出样例】
 13
【数据规模】
对于20%数据,1<=n<=10;
对于100%数据,1<=rmb<=100,1<=rp<=100,1<=time<=1000;
对于100%数据,1<=m<=100,1<=r<=100,1<=n<=100.
【提交链接】
http://www.rqnoj.cn/Submit.asp

【问题分析】

本题条件较多,需将问题简化,看能否找出熟悉的模型,如果我们只考虑钱够不够,或只考虑RP够不够,并且不考虑花费的时间,这样原问题可以简化成:在给定M元RMB(或R单位RP)的前题下,去泡足够多的MM,很显然这个问题就是典型的0/1背包问题。可以把泡MM用的RMB(或RP)看做重量,泡到MM的个数看做价值,给定的M(或R)就是背包的载重量。

但本题既要考虑RMB还要考虑RP怎么办呢?要是有足够的RMB去泡i号MM,而RP不够就泡不成了,要是RP够就可以。也就是在原来问题的基础上再加一维状态。

那要是再考虑上时间需最小怎么办呢?在求解过程中如果花X元RMP,Y单位RP可以到Z个MM,那么再泡i号MM时,发现可以用X-rmb[i]元,Y-rp[i]单位RP泡到的MM数加上这个MM(也就是+1)比原来Z多,就替换它(因为原则是尽量多的泡MM),如果和Z一样多,这时就要考虑原来花的时间多呢,还是现在花的时间多。要是原来的多,就把时间替换成现在用的时间(因为你既然可以泡到相同数量的MM,当然要尽量省点时间)。

设计一个二维状态opt[j,k]表示正好花j元RMP,k单位RP可以泡到的最多的MM的数量。增加一个辅助的状态ct[k,j]表示正好花j元RMP,k单位RP可以泡到的最多MM的情况下花费的最少的时间。

边界条件opt[0,0]=1 (按题意应该是0,但为了标记花费是否正好,设为1的话,这样opt[j,k]>0说明花费正好)

状态转移方程:

opt[j,k]:=max{opt[j-rmb[i],k-rp[i]]+1} (rmb[i]<=j<=m,rp[i]<=k<=r,0<i<=n,opt[j-rmb[i],k-rp[i]]>0)

ct[j,k]:=min{ct[j-rmb[i],k-rp[i]]}+time[i] (opt[j,k]=opt[j-rmb[i],k-rp[i]]+1)

【时间复杂度】

阶段数O(N)*状态数O(MR)*转移代价O(1)=O(NMR)

注:数据规模挺小的。

【问题拓展】如果要加入别的条件,比如泡MM还要一定的SP等,也就是说一个价值要不同的条件确定,那么这个问题的状态就需要再加一维,多一个条件就多一维。

program gf;
const
 fin='gf.in';
 fout='gf.out';
 maxn=110;
var
 rmb,rp,time:array[0..maxn] of longint;
 opt,ct:array[0..maxn,0..maxn] of longint;
 n,m,r,ans,max:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  for i:=1 to n do
   read(rmb[i],rp[i],time[i]);
  read(m,r);
  close(input);
 end;
procedure main;
 var
  i,j,k:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  fillchar(ct,sizeof(ct),0);
  opt[0,0]:=1;
  for i:=1 to n do
   for j:=m downto rmb[i] do
    for k:=r downto rp[i] do
     if opt[j-rmb[i],k-rp[i]]>0 then
      begin
       if opt[j-rmb[i],k-rp[i]]+1>opt[j,k] then
        begin
         opt[j,k]:=opt[j-rmb[i],k-rp[i]]+1;
         ct[j,k]:=ct[j-rmb[i],k-rp[i]]+time[i];
        end
       else if (opt[j-rmb[i],k-rp[i]]+1=opt[j,k])
             and (ct[j-rmb[i],k-rp[i]]+time[i]<ct[j,k]) then
              ct[j,k]:=ct[j-rmb[i],k-rp[i]]+time[i];
      end;
  max:=0;
  for j:=1 to m do
   for k:=1 to r do
    if opt[j,k]>max then
     begin
      max:=opt[j,k];
      ans:=ct[j,k];
     end
   else if (opt[j,k]=max) and (ct[j,k]<ans) then
    ans:=ct[j,k];
 end;
procedure print;
 begin
  writeln(ans);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:背包问题拓展-多多看DVD

【问题描述】
多多明天入学,今晚叔叔决定给他买一些动画片DVD晚上看,可爷爷规定他们只能在一定的时间段L内看完(因为叔叔忙完才能陪多多看碟,而多多每天很早就困了所以只能在一定的时间段里看碟)。多多列出了一张表,要叔叔给她买N张DVD碟,编号为1,2,3……N。多多给每张碟都打了分Mi(Mi>0),打分越高的碟说明多多越爱看,每张碟的播放时长为Ti。多多想在今晚爷爷规定的时间里看的碟总分最高(选择看的碟不能只看一半)。显然叔叔在买碟时没必要把N张全买了,只要买要看即可。而且多多让叔叔惯的特别任性,只要他看到有几张就一定会看完。可是出现了一个奇怪的问题,买碟的地方只买给顾客M(M<N)张碟,不会多也不会少。这可让多多叔叔为难了。如何在N张碟中只买M张而且在规定时间看完,而且使总价值最高呢?
【输入说明】(watchdvd.in)
输入文件有三行
第一行:正整数N,M,L(分别表示多多要叔叔买的碟的数量,商店要买给叔叔的碟的数量,爷爷规定的看碟的时间段)
第二行到第N行,每行两个数:Ti,Mi,给出多多列表中DVD碟的信息
【输出说明】(watchdvd.out)
单独输出一行,表示多多今晚看的碟的总分,如果商店卖给叔叔的M张碟无法在爷爷规定的时间看完输出0
【输入样例】
3 2 10
11 100
1 2
9 1
【输出样例】
3
【数据范围】
20%的数据   N <=20;  L<=2000;
100%的数据  N<=100  L<=2000; M <N
【时限】
1S
【提交链接】
 http://www.rqnoj.cn/

【问题分析】

本题比一般背包问题多了一个条件:不仅背包重量有限,连个数也有限。可能开始的思路是:在DP的过程中记录方案背包的个数,求解时当个数等于M即可。但该想法存在错误,因为当M不同时,最优解会不同,对应不同的M有不同的最优解,也就是一个不限制M的较优解可能是限制了M以后的最优解。

正确的解法:在原来背包问题求解的基础上,在状态上多加一维,表示不同的M对应的不同的最优解。设计状态opt[i,j]表示背包载重是j时,选取物品限制i个的最优解。状态转移方程:opt[i,j]=max(opt[i-1,j-w[i]]+v[i])。

【时间复杂度】

阶段数O(N)*状态数O(LM)+转移代价O(1)=O(NML)

program bb;
const
 fin='watchdvd.in';
 fout='watchdvd.out';
 maxn=100;
 maxL=1000;
var
 opt:array[0..maxn,0..maxL] of longint;
 w,val:array[0..maxn] of longint;
 n,m,v,ans:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(n,m,v);
  for i:=1 to n do
   read(w[i],val[i]);
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
procedure main;
 var
  i,j,k:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for i:=1 to n do
   for j:=m downto 1 do
    if j<=i then
     for k:=v downto w[i] do
      if (opt[j-1,k-w[i]]>0) or ((j=1) and (k=w[i])) then
       opt[j,k]:=max(opt[j-1,k-w[i]]+val[i],opt[j,k]);
   ans:=-maxlongint;
   for i:=0 to v do
    if opt[m,i]>ans then ans:=opt[m,i];
 end;
procedure print;
 begin
  write(ans);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:区间上的动态规划-石子合并问题

【问题描述】

在一个圆形操场的四周摆放着n堆石子。现要将石子有次序地合并成一堆。规定每次只能选相邻的2堆石子合并成新的一堆,并将新的一堆石子数记为该次合并的代价。试设计一个算法,计算出将n堆石子合并成一堆的最小代价。
在一个操场上摆放着一圈共n堆的石子。现要将石子有序地合并成一堆。规定每次只能选相邻的两堆合并成新的一堆,并将新的一堆石子数记为该次合并的得分。请编辑计算出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分。
【输入文件】
输入第一行为n(n<1000),表示有n堆石子,第二行为n个用空格隔开的整数,依次表示这n堆石子的石子数量m(m<=1000)
【输出文件】
输出将n堆石子合并成一堆的最小得分和将n堆石子合并成一堆的最大得分
【输入样例】
3
1 2 3
【输出样例】
9 11

【问题分析】

本题第一想法是贪心(找最大/最小的堆合并),但很容易找到贪心的反例(以求最小为例):

贪心:
3  4  6  5  4  2
5  4  6  5  4        得分:5
9  6  5  4            得分:9
9  6  9                得分:9
15  9                  得分:15
24                      得分:24
总得分:62

合理方案:
3  4  6  5  4  2
7  6  5  4  2        得分:7
7  6  5  6            得分:6
7  11  6              得分:11
13  11                得分:13
24                      得分:24
总得分:61

可见第二个4和2合并要比和5合并好,但由于之前2被用于和3合并,第二个4只好和5合并……既然贪心不行,数据规模又很大,自然要想到用动态规划。

  • 阶段:石子的每一次合并过程,先两堆合并,再三堆合并,...最后N堆合并
  • 状态:s[i,j]表示从编号为i的石头开始合并j堆
  • 决策:把当前阶段的合并方法细分成前一阶段已计算出的方法,选择其中的最优方案
  1. 第一阶段:两堆合并过程如下,其中sum(i,j)表示从i开始数j个数的和s[1,2]=s[1,1]+s[2,1]+sum(1,2)
    s[2,2]=s[2,1]+s[3,1]+sum(2,2)
    s[3,2]=s[3,1]+s[4,1]+sum(3,2)
    s[4,2]=s[4,1]+s[5,1]+sum(4,2)
    s[5,2]=s[5,1]+s[6,1]+sum(5,2)
    s[6,2]=s[6,1]+s[1,1]+sum(6,2)
  2. 第二阶段:三堆合并可以拆成两两合并,拆分方法有两种,前两个为一组或后两个为一组
    s[1,3]=s[1,2]+s[3,1]+sum(1,3)
    s[1,3]=s[1,1]+s[2,2]+sum(1,3)
    s[2,3]=s[2,2]+s[4,1]+sum(2,3)
    s[1,3]=s[2,1]+s[3,2]+sum(2,3)
  3. 第三阶段:四堆合并的拆分方法用三种,同理求出三种分法的得分,取其最优即可。以后第四阶段、第五阶段依次类推,最后在最后阶段中找出最优答案即可
  • 状态转移方程:F[i,j]=max(f[i][k-i]+f[k][j-(k-i)])+sum[i,j]
  • 要确定在某个状态下合并后得分最优,就需要知道合并它的总得分,而只有对子问题的总得分进行最优的判断,设计的状态才有意义,但要是想知道总分,就要知道合并一次的得分,显然这个含义不能加到动态规划的状态中,因为一般一个状态只代表一个含义(也就是说opt[i]的值只能量化一个问题)

先将本题中的环变成链(能更好分析问题):把环截断,复制一份放到截断后形成的链的后面,形成一个长度是原来两倍的链(只有环中的元素在处理时不发生变化才可以这样做。其实输入数据已经是截断的),比如‘3 4 5’变成‘3 4 5 3 4 5’,对于这样一个链,设计一个状态opt[i,j]表示起点为i终点为j的链合并成一堆所得到的最优得分。

要合并一个区间里的石子,无论合并的顺序如何,它的得分都是这个区间内所有石子的和,所以可以用一个数组sum[i]存合并前i个石子的得分。因为合并是连续的,所以决策就是把某次合并看作是把某个链分成两半,先把两半各自的好多堆分别合并成一堆,求总得分,再加最后合并这两半的得分。状态转移方程为:

maxopt[i,j]=max{maxopt[i,k]+maxopt[k+1,j]}+sum[j]-sum[i-1]
minopt[i,j]=min{minopt[i,k]+minopt[k+1,j]}+sum[j]-sum[i-1]

【时间复杂度】

状态数O(N^2)*决策数O(N)=O(N^3)

program stone;
const
 maxn=1010;
var
 a,sum:array[0..maxn] of longint;
 minopt,maxopt:array[0..maxn*2,0..maxn*2] of longint;
 n:longint;
 minans,maxans:longint;
procedure init;
 var
  i:longint;
 begin
  read(n);
  for i:=1 to n do
   begin
    read(a[i]);
    a[n+i]:=a[i];
   end;
  for i:=1 to n*2 do
   sum[i]:=sum[i-1]+a[i];
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
function min(x,y:longint):longint;
 begin
  min:=y;
  if (x<y) and (x>0) then min:=x;
 end;
procedure main;
 var
  i,j,L,k:longint;
 begin
  fillchar(minopt,sizeof(minopt),200);
  fillchar(maxopt,sizeof(maxopt),0);
  for i:=1 to 2*n do
   minopt[i,i]:=0;
  for L:=1 to n-1 do
   for i:=1 to 2*n-L do
    begin
     j:=i+L;
     for k:=i to j-1 do
      begin
       maxopt[i,j]:=max(maxopt[i,j],maxopt[i,k]+maxopt[k+1,j]);
       minopt[i,j]:=min(minopt[i,j],minopt[i,k]+minopt[k+1,j]);
      end;
     inc(maxopt[i,j],sum[j]-sum[i-1]);
     inc(minopt[i,j],sum[j]-sum[i-1]);
    end;
  maxans:=-maxlongint;
  minans:=maxlongint;
  for i:=1 to n do
   maxans:=max(maxans,maxopt[i,i+n-1]);
  for i:=1 to n do
   minans:=min(minans,minopt[i,i+n-1]);
  {for i:=1 to n*2 do
   begin
    for j:=1 to n*2 do
     write(maxopt[i,j],' ');
    writeln;
   end;}
 end;
begin
 init;
 main;
 writeln(minans,' ',maxans);
end.

例题:区间上的动态规划-能量项链

【问题描述】
在Mars星球上,每个Mars人都随身佩带着一串能量项链。在项链上有N颗能量珠。能量珠是一颗有头标记与尾标记的珠子,这些标记对应着某个正整数。并且,对于相邻的两颗珠子,前一颗珠子的尾标记一定等于后一颗珠子的头标记。因为只有这样才能通过吸盘将两颗珠子聚合成一颗珠子,同时释放出可以被吸盘吸收的能量。如果前一颗能量珠的头标记为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。
【输入文件】
输入文件energy.in的第一行是一个正整数N(4≤N≤100),表示项链上珠子的个数。第二行是N个用空格隔开的正整数,所有的数均不超过1000。第i个数为第i颗珠子的头标记(1≤i≤N),当i<N时,第i颗珠子的尾标记应该等于第i+1颗珠子的头标记。第N颗珠子的尾标记应该等于第1颗珠子的头标记。至于珠子的顺序,你可以这样确定:将项链放到桌面上,不要出现交叉,随意指定第一颗珠子,然后按顺时针方向确定其他珠子的顺序。
【输出文件】
输出文件energy.out只有一行,是一个正整数E(E≤2.1*109),为一个最优聚合顺序所释放的总能量。
【输入样例】
4
2  3  5  10
【输出样例】
710

【问题分析】

本题是经典的石子合并问题的变形,考点依然是区间上的动态规划。思考这个问题之前先得解决项链的环状怎么处理,按照题意,可以枚举切断点,把环状处理成链状。当然更好的方法是把环从任意一点切断,复制成两条链把这两条链首尾向接,针对题的读入我们直接把读入数据复制后连起来即可,如将‘2 3 5 10’变成‘2 3 5 10 2 3 5 10’,这样处理后其中任意长度为N+1的链就可代表一个环,那么问题就转化成合并任意长度为N+1的链所能释放的总能量最大。

也就是说从任意一点k(i<k<j),把链拆成两段问题的解就是:合并这两段各自所含珠子后释放出的最大能量,再加上对合并后的这两颗珠子进行再一次合并所释放的能量。将这个子问题进一步分解就是分解到链长度为1也就是就有两颗珠子时,这两颗珠子本身没有释放能量,而合并他们释放的能量是m*r*n(这就是边界条件)。

因此,设计一个状态opt[i,j]表示合并头为i,尾为j的链状项链所能释放的最多的能量值。边界条件是opt[i,i]=0 (1<=i<=n*2)。根据定义不难得到动规的状态转移方程为:opt[i,j]=max{opt[i,j],opt[i,k]+opt[k,j]+a[i]*a[k]*a[j]}(i<k<j)。

【时间复杂度】

此题有2N^2个状态,每个状态转移近似为N,所以时间复杂度为O(N^3),由于N很小所以瞬间就能出解。

program energy;
const
 fin='energy.in';
 fout='energy.out';
 maxn=300;
var
 a:array[0..maxn] of longint;
 opt:array[0..maxn,0..maxn] of longint;
 n,ans:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  readln(n);
  for i:=1 to n do
   begin
    read(a[i]);
    a[n+i]:=a[i];
   end;
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  if x>y then exit(x);
  exit(y);
 end;
procedure main;
 var
  i,j,k,L:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for L:=2 to n do
   for i:=1 to n*2-L+1 do
    begin
     j:=i+L;
     for k:=i+1 to j-1 do
      opt[i,j]:=max(opt[i,j],opt[i,k]+opt[k,j]+a[i]*a[j]*a[k]);
    end;
  for i:=1 to n do
   ans:=max(ans,opt[i,i+n]);
 end;
procedure print;
 begin
  writeln(ans);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

例题:统计单词个数

【问题描述】
给出一个长度不超过200的由小写英文字母组成的字符串(约定该字符串以每行20个字母的方式输入,且保证每行一定为20个)。要求将此字符串分成k份(1<k<=40),且每份中包含的单词个数加起来总数最大(每份中包含的单词可以部分重叠,当选用一个单词之后,其第一个字母不能再用。例如字符串this中可包含this和is,选用this之后,其它的就不能包含th)。单词在给出的一个不超过6个单词的字典中。
要求输出最大的个数。
【输入文件】
输入数据放在文本文件input3.dat中,其格式如下:
每组的第一行有二个正整数(p,k),p表示字符串的行数,k表示需分为k个部分。
接下来的p行,每行均有20个字符。
再接下来有一个正整数s,表示字典中单词个数(1<=s<=6)。
接下来的s行,每行均有一个单词。
【输出文件】
结果输出至屏幕,每行一个整数,分别对应每组测试数据的相应结果。
【输入样例】
1 3
thisisabookyouareaoh
4
is
a
ok
sab
【输出样例】
7

【问题分析】

题中说在一个串内对于一个固定了起点的单词只能用一次,即使其还可以构成别的单词但不能再用。比如字符串串thisa(字典为:this is th), 串中有‘this  is  th’这三个单词,但是对于this和th只能用其中一个,也就是说枚举一下构成单词的起点,只要以该起点的串中包含可以构成一个以该起点开头的单词,那么就说明这个串可以多包含一个单词。这样可得出下面的结果:

枚举的起点                结论
t                                 至少包含1个
h                                至少包含1个
i                                 至少包含2个
s                                至少包含2个
a                                至少包含2个

题目中要将串分成K个部分,也就是说从一个点截断后,一个单词就未必可以构成。比如上例要分成3个部分,其中合理的一个部分至多有3个字母,这样this这个单词就构不成了。若分成5个部分,那就连一个单词都构不成了。这样就需要对上面做个改动,上面的只控制了起点,而在题目中还需要限制终点,分完为几个部分后,每部分终点不同可以构成的单词就不同了。这样就需要再枚举终点了。

设计一个二维数组sum[i,j]统计从i到j的串中包含的单词的个数,状态转移方程为:

sum[i,j]=\left\{\begin{array}{lr}sum[i+1,j]+1\:(s[i,j]\:contains\:words\:starting\: with\:S[i]) & \\ sum[i+1,j]\:(s[i,j]\:doesn't\:contain\:words\:starting\:with\:S[i]) & \end{array} \right.

注:这里枚举字符的起点的顺序是从尾到头的。

求出所有的sum还差一步,就是不同的划分方法显然结果是不一样的,但是对于求解的问题,可以把原问题分解成子问题:求把一个串分成K部分的最多单词个数可以看做是先把串的最后一部分分出来,再把前面一部分分解成K-1个部分,显然决策就是找到一种划分的方法是前面的K-1部分的单词+最后一部分的单词最多。显然这个问题满足最优化原理,那是否满足无后效性呢?对于一个串分解出的最后一部分,其再分解前面的那部分是更本不会涉及分好的后面部分,即每次分解都会把串分解的更小,对于分解这个更小的串不会用到不属于这个小串的元素。这就满足无后效性。具体求解过程为:设计一个状态opt[i,j]表示把从1到j的串,分成i份可以得到最多的单词的个数。决策为:枚举分割点使当前这种分割方法可以获得最多的单词。

状态转移方程:opt[i,j]=max(opt[i-1,t]+sum[t+1,j]) (i<t<j)
边界条件:opt[1,i]=sum[1,i] (0<i<=L)

【复杂度】

时间复杂度:状态数O(N^2)*决策数O(N)=O(N^3)
空间复杂度:O(N^2)

program P3;
const
 fin='input3.dat';
 fout='output3.dat';
 maxn=210;
var
 s,ss:string;
 opt,sum:array[0..maxn,0..maxn] of longint;
 a:array[0..maxn] of string;
 n,ii,P,k,L,nn:longint;
procedure init;
 var
  i:longint;
 begin
  readln(p,k);
  s:='';
  for i:=1 to p do
   begin
    readln(ss);
    s:=s+ss;
   end;
  readln(n);
  for i:=1 to n do
   readln(a[i]);
 end;
function find(i,j:longint):boolean;
 var
  t:longint;
 begin
  for t:=1 to n do
   if pos(a[t],copy(s,i,j-i+1))=1 then exit(true);
  find:=false;
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
procedure main;
 var
  i,j,t:longint;
 begin
  L:=length(s);
  for i:=L downto 1 do
   for j:=i to L do
    if find(i,j) then sum[i,j]:=sum[i+1,j]+1
    else sum[i,j]:=sum[i+1,j];
  fillchar(opt,sizeof(opt),0);
  opt[1]:=sum[1];
  for i:=2 to k do
   for j:=i+1 to L do
    for t:=i+1 to j-1 do
     opt[i,j]:=max(opt[i,j],opt[i-1,t]+sum[t+1,j]);
  writeln(opt[k,L]);
 end;
begin
 assign(input,fin);
 reset(input);
 assign(output,fout);
 rewrite(output);
 init;
 main;
 close(input);
 close(output);
end.

例题:其他问题-花店橱窗设计

【问题描述】
假设以最美观的方式布置花店的橱窗,有F束花,每束花的品种都不一样,同时有V个花瓶(F<=V),被按顺序摆成一行,花瓶的位置是固定的,并从左到右,从1到V顺序编号,编号为1的花瓶在最左边,编号为V的花瓶在最右边,花束可以移动,并且每束花用1到F的整数惟一标识,标识花束的整数决定了花束在花瓶中列的顺序即如果I<J,则花束I必须放在花束J左边的花瓶中。例如,假设杜鹃花的标识数为1,秋海棠的标识数为2,康乃馨的标识数为3,所有的花束在放入花瓶时必须保持其标识数的顺序,即:杜鹃花必须放在秋海棠左边的花瓶中,秋海棠必须放在康乃馨左边的花瓶中。如果花瓶的数目大于花束的数目,则多余的花瓶必须空,即每个花瓶中只能放一束花。
每一个花瓶的形状和颜色也不相同,因此,当各个花瓶中放入不同的花束时会产生不同的美学效果,并以美学值(一个整数)来表示,空置花瓶的美学值为0。在上述例子中,花瓶与花束的不同搭配所具有的美学值,可以用如下表格表示。根据表格,杜鹃花放在花瓶2中,会显得非常好看,但若放在花瓶4中则显得很难看。为取得最佳美学效果,必须在保持花束顺序的前提下,使花的摆放取得最大的美学值,如果具有最大美学值的摆放方式不止一种,则输出任何一种方案即可。题中数据满足下面条件:1≤F≤100,F≤V≤100,-50≤Aij≤50,其中Aij是花束i摆放在花瓶j中的美学值。输入整数F,V 和矩阵(Aij),输出最大美学值和每束花摆放在各个花瓶中的花瓶编号。
┌────┬────┬────┬────┬────┬────┐
│           │花瓶1 │花瓶2 │花瓶3  │花瓶4 │花瓶5 │
├────┼────┼────┼────┼────┼────┤
│杜鹃花│   7     │  23     │ -5      │ -24     │ 16     │
├────┼────┼────┼────┼────┼────┤
│秋海棠│   5     │  21     │ -4      │ 10      │  23    │
├────┼────┼────┼────┼────┼────┤
│康乃馨│  -21   │  5       │  -4     │  -20   │  20     │
└────┴────┴────┴────┴────┴────┘
【输入文件】
第一行包含两个数:F,V。随后的F行中,每行包含V个整数,Aij即为输入文件中第i+1行中的第j个数
【输出文件】
包含两行,第一行是程序所产生摆放方式的美学值。第二行必须用F个数表示摆放方式,即该行的第K个数表示花束K所在的花瓶的编号。
【输入样例】
3 5 
7 23 –5 –24 16 
5 21 -4 10 23 
-21 5 -4 -20 20 
【输出样例】
53 
2 4 5 
【题目链接】
http://mail.bashu.cn:8080/JudgeOnline/showproblem?problem_id=1597

【问题分析】

常规思维是拿来花一个一个地放,假设要放第i束花时,根据贪心策略考虑其放置位置,找到一个符合条件(第i束花要放在前i-1束花的后面)的位置且产生最大的美学价值的花瓶放。但很容易举出反例:

│           │花瓶1 │花瓶2  │花瓶3│
├────┼────┼────┼────|
│杜鹃花│   1     │  2       │ -5      |
├────┼────┼────┼────|
│秋海棠│   5     │  10     │  1      │

按照贪心策略是:杜鹃花放在2号瓶里,秋海棠放在3号瓶里,美学值:3;而答案是:杜鹃花放在1号瓶里,秋海棠放在2号瓶里,美学值:11。

数据量很大搜索显然不行。需考虑动态规划,那阶段、状态、决策分别是什么呢?既然要拿来花束一个一个地放,就以花束划分阶段。设计一个状态opt[i,j]表示将第i束花放在第j个花瓶中可使前i束花获得的最大美学价值;决策就是:将第i束花放在第j个瓶中,那么第i-1束花只能放在前j-1个瓶里,显然是要找到一个放在前j-1个瓶中的一个最大的美学价值再加上当前第i束放在第j个瓶中的美学价值就是opt[i,j]的值。显然符合最优化原理和无后效性。状态转移方程为:opt[i,j]=max{opt[i-1,k]}+a[i,j] (i<=k<=j-1)。

【复杂度】

状态数O(FV)*转移代价O(V)=O(FV^2),数据范围很小,可以在瞬间出解。

program P1597;
const
 maxn=110;
var
 a,opt,path:array[0..maxn,0..maxn] of longint;
 n,m,ans:longint;
procedure init;
 var
  i,j:longint;
 begin
  read(n,m);
  for i:=1 to n do
   for j:=1 to m do
    read(a[i,j]);
 end;
procedure main;
 var
  i,j,k:longint;
 begin
  for i:=1 to n do
   for j:=1 to m do
    opt[i,j]:=-maxlongint;
  for i:=1 to n do
   for j:=i to m-n+i do
    begin
     for k:=i-1 to j-1 do
      if opt[i-1,k]>opt[i,j] then
       begin
        opt[i,j]:=opt[i-1,k];
        path[i,j]:=k;
       end;
     inc(opt[i,j],a[i,j]);
    end;
  ans:=n;
  for i:=n+1 to m do
   if opt[n,i]>opt[n,ans]then ans:=i;
 end;
procedure outputway(i,j:longint);
 begin
  if i>0 then
   begin
    outputway(i-1,path[i,j]);
    write(j,' ');
   end;
 end;
procedure print;
 var
  i:longint;
 begin
  writeln(opt[n,ans]);
  outputway(n,ans);
  writeln;
 end;
begin
 init;
 main;
 print;
end.

例题:其他问题-Divisibility

【问题描述】
给出N个数,在这N个数中任意地添加+号或-号,求出能不能使算出的结果被K整除。可以则打印“Divisible”,否则打印“Not divisible”,例如(给定4个数,分别是17 5 -21 15):
17 + 5 + -21 + 15 = 16
17 + 5 + -21 - 15 = -14
17 + 5 - -21 + 15 = 58
17 + 5 - -21 - 15 = 28
17 - 5 + -21 + 15 = 6
17 - 5 + -21 - 15 = -24
17 - 5 - -21 + 15 = 48
17 - 5 - -21 - 15 = 18
有8种添法,其中第二种求出的-14能被7整除。
【输入文件】
第一行为两个整数N和K(1 <= N <= 10000, 2 <= K <= 100)
第二行包含N个整数,均不超过10000
【输出文件】
可以则打印“Divisible”,否则打印“Not divisible”
注意:输出每组结果之间有空格,最后一行无空格
【输入样例】
2
4 7
17 5 -21 15
4 5
17 5 -21 15
【输出样例】
Divisible
Not divisible

【问题分析】

常规思路就是枚举中间添的运算符,算出值再MOD K,如果有一个值MOD K=0则输出“Divisible”。时间复杂度是O(2N-1)。但本题给出的数据量很大,这样做效率很低。

因为题目涉及MOD运算,要想简化问题就需要知道一些基本的MOD运算性质:

A*B mod C=(A mod C * B mod C) mod C
(A+B) mod C=(A mod C + B mod C) mod C

有了这个性质,我们就可以把累加后求余转化成求余后累加(并把减法看作加负数,全变为加法操作)再求余。这样操作的数据就控制在了1-K~K-1的范围内了。然后要判断的就是,所有结果的累加和MOD K是否为0。简记为:(A+B) mod K=0 or (A+B) mod K<>0。

如果我们按数的个数划分阶段,前N-1个数的运算结果MOD K看做A,第N个数看作B即可。于是状态可设计为:opt[i,j]表示前i个数是否可以得到余数为j的结果。那么状态转移方程就是:

opt[i,(j-a[i] mod k )mod k]=opt[i-1,j] (opt[i-1,j]=true)
opt[i,(j+a[i] mod k) mod k]=opt[i-1,j] (opt[i-1,j]=true)

如果opt[n,0]=true就输出‘Divisible’。

program P2042;
const
 maxk=110;
 maxn=10010;
var
 a:array[0..maxn] of longint;
 opt:array[1..2,-maxk..maxk] of boolean;
 n,k,tim,ii:longint;
 vis:array[0..maxn] of boolean;
procedure init;
 var
  i:longint;
 begin
  read(n,k);
  for i:=1 to n do
   read(a[i]);
 end;
procedure main;
 var
  i,j,p1,p2,p3:longint;
 begin
  fillchar(opt,sizeof(opt),false);
  fillchar(vis,sizeof(vis),false);
  for i:=1 to n do
   if a[i] mod k=0 then vis[i]:=true;
  for i:=1 to n do
   a[i]:=a[i] mod k;
  opt[1,a[1]]:=true;
  p1:=1;
  p2:=2;
  for i:=2 to n do
   if not vis[i] then
    begin
     fillchar(opt[p2],sizeof(opt[p2]),false);
     for j:=1-k to k-1 do
      if opt[p1,j] then
       begin
        opt[p2,(j-a[i]) mod k]:=true;
        opt[p2,(j+a[i]) mod k]:=true;
       end;
     p3:=p1;
     p1:=p2;
     p2:=p3;
    end;
 if opt[p1,0] then writeln('Divisible')
 else writeln('Not divisible');
 end;
begin
 read(tim);
 for ii:=1 to tim do
  begin
   if ii>1 then
   writeln;
   init;
   main;
  end;
end.

例题:多维动态规划优化/矩阵问题-盖房子

【问题描述】
小明最近得到一块面积为n\timesm的土地,他想在这块土地上建造一所正方形的房子,但这块土地并非十全十美,上面有很多不平坦的地方(瑕疵),以至于根本不能在上面盖一砖一瓦。他希望找到一块最大的正方形且无瑕疵的土地来盖房子。
【输入文件】
输入文件第一行为两个整数n,m(1<=n,m<=1000),接下来n行,每行m个数字,用空格隔开。0表示该块土地有瑕疵,1表示该块土地完好。
【输出文件】
一个整数,最大正方形的边长。
【输入样例】
4 4
0 1 1 1
1 1 1 0
0 1 1 0
1 1 0 1
【输出样例】
2

【问题分析】

题中说要求一个最大的符合条件的正方形,所以就想到判断所有的正方形是否合法。直观的状态表示法是opt[i,j,k],基类型是boolean,判断以(i,j)点为左上角(其实任意一个角都可以,依据个人习惯),长度为K的正方形是否合理,再找到一个K值最大的合法状态就可以了(用true表示合理,false表示不合理)。其实这就是递推(决策唯一),其递推式为:

opt[i,j,k]=opt[i+1,j+1,k-1] and opt[i+1,j,k-1] and opt[i,j+1,k-1] and (a[i,j]=1)

其时间复杂度为:状态数O(N^3)*转移代价O(1)=总复杂度O(N^3);空间复杂度为:O(N^3)。空间复杂度和时间复杂度均较高,可以考虑用滚动数组优化空间,但是时间复杂度仍然是O(N^3),这就需要找另外一种简单的状态表示法求解了。

仔细分析这个题目,其实我们没必要知道正方形的所有长度,只要知道以一个点为左上角的正方形的最大合理长度就可以了。如果这个左上角是0那么它的最大合理长度自然就是0(不可能合理)。如果这个左上角是1呢?上面的递推式考虑的是以其右面、下面、右下这三个方向的正方形是否合理,所以这里依然还是要考虑这三个方向。具体怎么考虑呢?如果这三个方向合理的最大边长中一个最小的是X,那么它的最大合理边长就是X+1。为什么呢?看个例子:

0 1 1 1 1 1
1 1 1 1 1 1
0 1 0 1 1 0
1 1 0 1 1 1

以(1,3)为左上角,分别以(1,4)、(2,3)、(2,4)为左上角的最大合理边长分别是2,1,2。其中最小的是以(2,3)为左上角的正方形,最大合理边长是1。因为三个方向的最大合理边长大于等于1,所以三个方向上边长为1的正方形是合理的,即上面递推式中:

opt[1,3,2]=opt[1,4,1] and opt[2,3,1] and opt[2,4,1] and (a[1,3]=1) = true 成立

这样就把一个递推判定性问题转化成最优化问题从而节省空间和时间。具体实现,设计一个状态opt[i,j]表示以(i,j)为左上角的正方形的最大合理边长。状态转移方程为:

opt[i,j]=\left\{\begin{array}{lr}min\{ opt[i+1,j],opt[i,j+1],opt[i+1,j+1] \} +1\:(a[i,j]=1) & \\ 0\:(a[i,j]=0) & \end{array} \right.

【复杂度】

时间复杂度:状态数O(N^2)*转移代价O(1)=总代价O(N^2);空间复杂度:O(N^2)

program P1057;
const
 maxn=1010;
var
 opt,a:array[0..maxn,0..maxn] of longint;
 n,m,ans:longint;
procedure init;
 var
  i,j:longint;
 begin
  read(n,m);
  for i:=1 to n do
   for j:=1 to m do
    read(a[i,j]);
 end;
procedure main;
 var
  i,j:longint;
 begin
  fillchar(opt,sizeof(opt),0);
  for i:=n downto 1 do
   for j:=m downto 1 do
    if a[i,j]<>0 then
     begin
      opt[i,j]:=opt[i+1,j];
      if opt[i,j+1]<opt[i,j] then
       opt[i,j]:=opt[i,j+1];
      if opt[i+1,j+1]<opt[i,j] then
       opt[i,j]:=opt[i+1,j+1];
      inc(opt[i,j]);
     end;
  ans:=0;
  for i:=1 to n do
   for j:=1 to m do
    if opt[i,j]>ans then ans:=opt[i,j];
  writeln(ans);
 end;
begin
 init;
 main;
end.

例题:多进程动态规划-方格取数

【问题描述】
设有N*N的方格图(N<=10,将其中的某些方格中填入正整数,而其他的方格中则放入数字0。如下图所示,某人从图的左上角的A点出发,可以向下行走,也可以向右走,直到到达右下角的B点。在走过的路上,他可以取走方格中的数(取走后,对应方格将变为数字0)。此人从A点到B点共走两次,试找出2条这样的路径,使得取得的数之和为最大。
【输入文件】
输入的第一行为一个整数N(表示N*N的方格图),接下来的每行有三个整数,前两个表示位置,第三个数为该位置上所放的数。一行单独的0表示输入结束。
【输出文件】
只需输出一个整数,表示2条路径上取得的最大的和。
【输入样例】
8
2  3  13
2  6  6
3  5  7
4  4  14
5  2  21
5  6  4
6  3  15
7  2  14
0  0  0
【输出样例】
67

【问题分析】

本题是经典的多进程动态规划题,如果只取一次,其模型就是前面讲的街道问题。但现在要求取两次,怎么办呢?常规思路是:将前面取过的数全赋成0,然后再取一次。但这样做是错的,第一次取的显然是最大值,但第二次取的未必次大,所以也许两条非最大的路比一条最大一条小一点的路更优。既然这样做不行,那还得回到动态规划的本质来看问题,本题中,对于走一次,走到矩阵的任意一个位置就是一个状态,而要是走两次,显然走到矩阵的某个位置只是一个状态的一部分,不能完整的描述整个状态。那另一部分显然就是第二次走到的位置了。如果我们把这两部分合起来就是一个完整的状态了。

于是,设计一个状态opt[i1,j1,i2,j2]表示两条路分别走到(i1,j1)点和(i2,j2)点时取到的最大值。显然决策有4中(乘法原理一个点两种*另一个点两种),即(上,上)(上,左)(左,上)(左,左),其中上和左表示从哪个方向走到该点,当然要注意走到同行、同列、同点时的情况(因为要求路径不重复)。其状态转移方程为:

opt[i,j]=\left\{\begin{array}{lr}max(opt[i1-1,j1,i2-1,j2],opt[i1,j1-1,i2-1,j2])+a[i1,j1]+a[i2,j2]\:(1\leq i1= i2\leq n,1\leq j1\leq j2\leq n)& \\ max(opt[i1-1,j1,i2,j2-1],opt[i1,j1-1,i2,j2-1])\:(1\leq j1=j2\leq n,1\leq i1\leq i2\leq n)& \\ max(opt[i1-1,j1,i2-1,j2],opt[i1-1,j1,i2,j2-1],opt[i1,j1-1,i2-1,j2],opt[i1,j1-1,i2,j2-2]+a[i1,j1]+a[i2,j2])\:(1\leq i1,j1\leq i2,j2\leq n)& \\ max(opt[i1-1,j1,i2-1,j2],opt[i1-1,j1,i2,j2-1],opt[i1,j1-1,i2-1,j2],opt[i1,j1-1,i2,j2-2]+a[i1,j1])\:(1\leq i1=i2\leq n,1\leq j1=j2\leq n)& \end{array} \right.

【复杂度】

时间复杂度:状态数O(N^4)*转移代价O(1)=总复杂度O(N^4)
空间复杂度:O(N^4)

program fgqs;
const
 fin='fgqs.in';
 fout='fgqs.out';
 maxn=11;
var
 a:array[0..maxn,0..maxn] of longint;
 opt:array[0..maxn,0..maxn,0..maxn,0..maxn] of longint;
 n:longint;
procedure init;
 var
  i,j,w:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  repeat
   readln(i,j,w);
   a[i,j]:=w;
  until i=0;
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
procedure main;
 var
  i1,i2,j1,j2:longint;
 begin
  for i1:=1 to n do
   for j1:=1 to n do
    for i2:=i1 to n do
     for j2:=1 to j1 do
      if (i1=i2) and (j1=j2) then
       opt[i1,j1,i2,j2]:=opt[i1-1,j1,i2,j2-1]+a[i1,j1]
      else if (i1=i2-1) and (j1=j2) then
       opt[i1,j1,i2,j2]:=max(opt[i1-1,j1,i2,j2-1],opt[i1,j1-1,i2,j2-1])
+a[i1,j1]+a[i2,j2]
      else if (i1=i2) and (j1=j2+1) then
       opt[i1,j1,i2,j2]:=max(opt[i1-1,j1,i2,j2-1],opt[i1-1,j1,i2-1,j2])
+a[i1,j1]+a[i2,j2]
      else begin
            opt[i1,j1,i2,j2]:=max(opt[i1-1,j1,i2,j2-1],opt[i1-1,j1,i2-1,j2]);
            opt[i1,j1,i2,j2]:=max(opt[i1,j1,i2,j2],opt[i1,j1-1,i2,j2-1]);
            opt[i1,j1,i2,j2]:=max(opt[i1,j1,i2,j2],opt[i1,j1-1,i2-1,j2]);
            inc(opt[i1,j1,i2,j2],a[i1,j1]+a[i2,j2]);
           end;
 end;
procedure print;
 begin
  writeln(opt[n,n,n,n]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

如果本题的数据范围再大点就得优化了,怎么优化这个程序呢?上面说过对于时间空间都大的时候,首先想到的就是寻找特点,改变状态的表示法,减少状态的维数。仔细分析可发现,处于同行、同列的状态,等价于另外一个点在对角线上的状态。而这条对角线正是此题的阶段。因为在状态转移的时候,后面的那个点总是从固定的一个方向转移来的。也就是说只要对角线上的状态就可以省掉那些同行同列的状态了。类似于N皇后如何表示右上到左下的这几条对角线,对于一个点(i,j),其对角右上角的点就是(i-1,j+1),所以可以看出这些点的和是定值,且值从2到N*2。这样用三个变量就可以表示这两个点了,于是设计状态opt[k,i1,i2]表示处于阶段k时走到i1,i2的两条路径所取得的数的最大和。状态转移方程为:

opt[k,i1,i2]=\left\{\begin{array}{lr}max(opt[k-1,i1-1.i2-1],opt[k-1,i1-1,i2],opt[k-1,i1,i2-1],opt[k-1,i1,i2])+a[i1,k-i1]+a[i2,k-i2]\:(1\leq i1,i2\leq n,2\leq k\leq n^2,i1\neq i2)& \\opt[k-1,i1-1,i2]+a[i1,k-i1]\:(1\leq i1,i2\leq n,2\leq k\leq n^2,i1=i2)& \end{array} \right.

program fgqs;
const
 fin='fgqs.in';
 fout='fgqs.out';
 maxn=11;
var
 a:array[0..maxn,0..maxn] of longint;
 opt:array[0..maxn*2,0..maxn,0..maxn] of longint;
 n:longint;
procedure init;
 var
  i,j,w:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  repeat
   readln(i,j,w);
   a[i,j]:=w;
  until i=0;
  close(input);
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
procedure main;
 var
  k,i1i2,j1,j2,mid:longint;
 begin
  for k:=2 to n*2 do
   begin
    for i1:=1 to n do
     if (k-i1>0) and (k-i1<=n) then
      for i2:=1 to n do
       if (k-i2>0) and (k-i2<=n) then
        begin
         if i1=i2 then
          opt[k,i1,i2]:=opt[k-1,i1-1,i2]+a[i1,k-i1]
         else begin
               opt[k,i1,i2]:=max(opt[k-1,i1,i2],opt[k-1,i1,i2-1]);
               opt[k,i1,i2]:=max(opt[k,i1,i2],opt[k-1,i1-1,i2]);
               opt[k,i1,i2]:=max(opt[k,i1,i2],opt[k-1,i1-1,i2-1]);
               inc(opt[k,i1,i2],a[i1,k-i1]+a[i2,k-i2]);
              end;
        end;
   end;
 end;
procedure print;
 begin
  writeln(opt[n*2,n,n]);
  close(output);
 end;
begin
 init;
 main;
 print;
end.

注意:若取多次和取两次道理一样,每多一次状态就多加一维,但如果数据范围很大,时间空间复杂度太高,可以用网络流解此题。

例题:树型动态规划-加分二叉树

【问题描述】
设一个n个节点的二叉树tree的中序遍历为(1,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第i个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
    subtree的左子树的加分×subtree的右子树的加分+subtree的根的分数
    若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
    试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出:
    (1)tree的最高加分
    (2)tree的前序遍历
【输入格式】
第1行:一个整数n(n<30),为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(分数<100)。
【输出格式】
第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
第2行:n个用空格隔开的整数,为该树的前序遍历。
【输入样例】
5
5 7 1 2 10
【输出样例】
145
3 1 2 4 5

【问题分析】

本题是典型的树型动态规划,而且题目中已经给出了加分的求法:subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数,一般首先考虑建树,但本题不用建树,题目给定树的中序遍历是1,2,3……N,求树的先序,这样马上就想到怎样用中序和先序确定一棵树:枚举树根i那么1,2……i-1就是它的左子树中的结点,i+1……N就是它右子树中的结点。这样一颗树按这样的递归定义就构造出来了(当然只要这个过程并不需要存储这棵树)。在构造过程中顺便求出加分来,在一个序列里不同的元素做根显然加分不同,我们只要记录一个最大的就可以了。具体实现方法:设计状态opt[L,r]表示以L为起点,以r为终点的结点所组成的树的最高加分,阶段就是树的层数。决策就是在这些结点中找一个结点做根使树的加分最大,状态转移方程为:

opt[L,r]=\left\{\begin{array}{lr}1\:(L>r)& \\ a[L]\:(L=r)& \\max\{opt[L,i-1]*opt[i+1,r]+a[i]\}\:(L<r,L\leq i\leq r)& \end{array} \right.

在保存最优解的过程用path[i,j]记录以i为起点,以j为终点的中序结点中的根即可。由于树型动态规划的阶段不明显所以一般用记忆化搜索来实现。

【时间复杂度】

状态数O(N^2)*转移代价O(N)=O(N^3)

program binary;
const
 fin='binary.in';
 fout='binary.out';
 maxn=100;
var
 a:array[0..maxn] of longint;
 path,opt:array[0..maxn,0..maxn] of longint;
 n:longint;
procedure init;
 var
  i:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  read(n);
  for i:=1 to n do
   read(a[i]);
  close(input);
  fillchar(opt,sizeof(opt),0);
 end;
function TreeDp(L,r:longint):longint;
 var
  i,x:longint;
 begin
  if opt[L,r]=0 then
   begin
    if L>r then
     begin
      opt[L,r]:=1;
      path[L,r]:=0;
     end
    else if L=r then
     begin
      opt[L,r]:=a[L];
      path[L,r]:=L;
     end
    else begin
          for i:=L to r do
           begin
            x:=TreeDp(L,i-1)*TreeDp(i+1,r)+a[i];
            if x>opt[L,r] then
             begin
              opt[L,r]:=x;
              path[L,r]:=i;
             end;
           end;
         end;
   end;
  TreeDp:=opt[L,r];
 end;
procedure print(L,r:longint);
 begin
  if path[L,r]>0 then
   begin
    write(path[L,r],' ');
    print(L,path[L,r]-1);
    print(path[L,r]+1,r);
   end;
 end;
begin
 init;
 writeln(TreeDp(1,n));
 print(1,n);
 close(output);
end.

例题:树型动态规划-A Binary Apple Tree/苹果二叉树

【问题描述】
设想苹果树很像二叉树,每一枝都是生出两个分支。我们用自然数来数这些枝和根那么必须区分不同的枝(结点),假定树根编号都是定为1,并且所用的自然数为1到N。N为所有根和枝的总数。例如下图的N为5,它是有4条枝的树。
  2   5 
    \ /  
    3   4 
      \ / 
       1 
当一棵树太多枝条时,采摘苹果是不方便的,这就是为什么有些枝要剪掉的原因。现在我们关心的是,剪枝时,如何令损失的苹果最少。给定苹果树上每条枝的苹果数目,及必须保留的树枝的数目。你的任务是计算剪枝后,能保留多少苹果。
【输入文件】
首行为N,Q (1 <= Q <= N, 1 < N <= 100), N为一棵树上的根和枝的编号总数,Q为要保留的树枝的数目。以下N-1行为每条树枝的描述,用3个空格隔开的整数表示,前2个数为树枝两端的编号,第三个数为该枝上的苹果数。假设每条枝上的苹果数不超过3000个。
【输出文件】
输出能保留的苹果数。(剪枝时,千万不要连根拔起)
【输入样例】
5 2 
1 3 1 
1 4 10 
2 3 20 
3 5 20 
【输出样例】
21
【提交链接】
http://acm.timus.ru/problem.aspx?space=1&num=1018

【问题分析】

本题为树型动态规划问题,是求最优值,不同于上一题的是,本题边有值并非点有值,而且本题需要建树。建树时要注意,给每个结点增加一个域SUM用来存以它为根的树中的边的数目。其实树型动态规划最好分析,因为树这种数据结构本来就符合递归定义,这样的话子问题很好找,显然这个问题的子问题就是一棵树要保留M个枝分三种情况:

剪掉左子树:让右子树保留M-1个枝可保留最多的苹果数+连接右子树的枝上的苹果数
剪掉右子树:让左子树保留M-1个枝可保留最多的苹果数+连接左子树的枝上的苹果数
都不剪掉:  让左子树保留i个枝,让右子树保留M-2-i个枝可保留最多的苹果数+连接右子树的枝上的苹果数+连接左子树的枝上的苹果数

显然边界条件就是如果要保留的树枝数比当前的子树的枝多,或着一个树要保留0个枝,则结果就是0。因为一颗树中根结点是对子树的完美总结,所以满足最优化原理。每次求解只和子树有关所以也满足无后效性,可用动态规划。设计一个状态opt[num,i]表示要让结点编号为i的树保留num个枝可得到的最优解。状态转移方程为:

opt[num,i]=max{opt[num-1,BT[i].L]+T[i,BT[i].L],opt[num-1,BT[i].r]+T[i,BT[i].r],opt[k,BT[i].L]+opt[num-2-k,BT[i].r]+T[i,BT[i].L]+T[i,BT[i].r]}  

(0<=k<=n-2,BT:树,T:读入时记录的枝上的苹果数);

【时间复杂度】

状态数O(NM)*转移代价O(M)=O(NM^2)

program P1018;
const
 fin='P1018.in';
 fout='P1018.out';
 maxn=110;
type
 treetype=record
           l,r,sum:longint;
          end;
var
 T,opt:array[0..maxn,0..maxn] of longint;
 BT:array[0..maxn] of treetype;
 vis:array[0..maxn] of boolean;
 n,m,p:longint;
procedure init;
 var
  i,j,k,w:longint;
 begin
  assign(input,fin);
  reset(input);
  assign(output,fout);
  rewrite(output);
  fillchar(T,sizeof(T),0);
  read(n,m);
  for k:=1 to n-1 do
   begin
    read(i,j,w);
    T[i,j]:=w;
    T[j,i]:=w;
   end
  close(input);
  fillchar(vis,sizeof(vis),false);
 end;
Procedure creat_tree(i:longint);
 var
  j:longint;
 begin
  vis[i]:=true;
  for j:=1 to n do
   if (T[i,j]>0) and (not vis[j]) then
    begin
     creat_tree(j);
     BT[i].L:=Bt[i].r;
     Bt[i].r:=j;
     inc(Bt[i].sum,BT[j].sum+1);
    end;
 end;
Function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
Function F(num,i:longint):longint;
 var
  k:longint;
 begin
  if opt[num,i]<0 then
   begin
    if (num>BT[i].sum) or (num=0) then opt[num,i]:=0
    else begin
          opt[num,i]:=F(num-1,BT[i].L)+T[i,BT[i].L];
          opt[num,i]:=max(opt[num,i],F(num-1,BT[i].r)+T[i,BT[i].r]);
          for k:=0 to num-2 do
           opt[num,i]:=max(opt[num,i],F(k,BT[i].L)+F(num-2-k,BT[i].r)
+T[i,BT[i].L]+T[i,BT[i].r]);
         end;
   end;
  F:=opt[num,i];
 end;
begin
 init;
 creat_tree(1);
 fillchar(opt,sizeof(opt),200);
 writeln(F(m,1));
 close(output);
end.

例题:选课

【问题描述】
学校实行学分制。每门的必修课都有固定的学分,同时还必须获得相应的选修课程学分。学校开设了N(N<300)门的选修课程,每个学生可选课程的数量M是给定的。学生选修了这M门课并考核通过就能获得相应的学分。在选修课程中,有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如《Frontpage》必须在选修了《Windows操作基础》之后才能选修。我们称《Windows操作基础》是《Frontpage》的先修课。每门课的直接先修课最多只有一门。多门课也可能存在相同的先修课。每门课都有一个课号,依次为1,2,3,…。例如,1是2的先修课,2是3、4的先修课。如果要选3,那么1和2都一定已被选修过。你的任务是为自己确定一个选课方案,使得你能得到的学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。
【输入文件】
输入文件的第一行包括两个整数N、M(中间用一个空格隔开)其中1≤N≤300,1≤M≤N。
以下N行每行代表一门课。课号依次为1,2,…,N。每行有两个数(用一个空格隔开),第一个数为这门课先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。学分是不超过10的正整数。
【输出文件】
输出文件只有一个数,实际所选课程的学分总数。
【输入样例】
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
【输出样例】
13

【问题分析】

一个课程可能是多个课程的先修课,可是一个课最多只有一个先修课。类似于数据结构中的树,而建立在树这种数据结构上的最优化问题,自然考虑树型动态规划。想到树型动态规划,那么第一步就是想这课树是否是二叉树,如果不是,是否需要转化呢?显然这个问题不是二叉树,又因为问题是在多个课程中选M个,也就是说是树中一棵或多棵子树的最优解,这样问题就需要转化成二叉树了。注意题目中说一个课程没有先修课是0,也就是说这课树的根是0。

把数据结构确定了以后就要想动态规划的三要素了。树型动态规划阶段具有共性:树的层数;状态是结点,但是只描述结点显然不够,需要再加一个参数。因此设计一个状态opt[i,j]表示以i为根的树,选j个课程可获得的最优解。因为树是以0为根的而0又必须要选所以问题的解不是opt[0,m]而是opt[0,m+1]。决策是什么呢?对于二叉树,在设计决策时可分类讨论:

<1>这棵树只有左子树:要选左子树中的课程,那根结点一定要选,所以决策就是在左子树中选j-1个课程,再加上选根结点可获分数。
<2>这棵树只有右子树:因为右子树和根在原问题中是兄弟关系,所以选右子树中的课程未必要选根,这样决策就有两条:(1)在右子树中选j个的最优值。(2)在右子树中选j-1个的最优值再加上选根结点可获得的分数。
<3>都有:这种情况的决策很容易想到,从左子树中选k-1个,从右子树中选j-k个的最优值加上根结点可获得的分数。但要注意,当K=1也就是左子树选0个时,根结点可以不选,右子树可以选j个而不是j-1个;当然根结点也可以选,选了根结点右子树就选j-1个。

针对不同情况写出状态转移方程:

opt[i,j]=\left\{\begin{array}{lr}max(opt[t[i].L,j-1]+t[i].data)\:(Only\:have\:the\:left \:subtree)& \\ max(opt[t[i].r,j-1]+t[i].data,opt[t[i].r,j])\:(Only\:have\:the\:right\:subtree)& \\max(opt[t[i].L,k-1]+opt[t[i].r,j-k]+t[i].data,opt[t[i].r,j])\:(All\:have)(1\leq k\leq j)& \end{array} \right.

【时间复杂度】

状态数O(NM)*转移代价O(M)=O(NM^2),其实际转移代价比理论值小得多。

program P1180;
const
maxn=400;
type
 treetype=record
           data,L,r:longint;
          end;
var
 T:array[0..maxn] of treetype;
 opt:array[0..maxn,0..maxn] of longint;
 n,m,ans:longint;
 F:array[0..maxn] of longint;
procedure init;
 var
  i,x,y:longint;
 begin
  read(n,m);
  fillchar(f,sizeof(f),0);
  for i:=0 to n do
   begin
    T[i].L:=-1;
    T[i].r:=-1;
   end;
  for i:=1 to n do
   begin
    read(x,y);                         {边读入边将多叉树转化成二叉树}
    T[i].data:=y;
    if F[x]=0 then T[x].L:=i
    else T[F[x]].r:=i;
    F[x]:=i;
   end;
  fillchar(opt,sizeof(opt),200);
  for i:=0 to n do
   opt[i,0]:=0;
 end;
function max(x,y:longint):longint;
 begin
  max:=y;
  if x>y then max:=x;
 end;
function TreeDp(i,j:longint):longint;
 var
  k:longint;
 begin
  if opt[i,j]<0 then
   begin
    if (T[i].L<0) and (T[i].r<0) then
     begin
      if j<>1 then opt[i,j]:=0
      else opt[i,j]:=T[i].data;
     end
    else if (T[i].L>=0) and (T[i].r<0) then
     opt[i,j]:=max(opt[i,j],TreeDp(T[i].L,j-1)+T[i].data)
    else if (T[i].L<0) and (T[i].r>=0) then
     begin
      opt[i,j]:=max(opt[i,j],TreeDp(T[i].r,j));
      opt[i,j]:=max(opt[i,j],TreeDp(T[i].r,j-1)+T[i].data);
     end
    else begin
          opt[i,j]:=max(opt[i,j],TreeDp(T[i].r,j));
          for k:=1 to j do
           opt[i,j]:=max(opt[i,j],TreeDp(T[i].L,k-1)+
TreeDp(T[i].r,j-k)+T[i].data);
         end;
   end;
  TreeDp:=opt[i,j];
 end;
begin
 init;
 writeln(TreeDp(0,m+1));
end.

例题:滑雪问题

【问题描述-poj1088】
Michael滑雪时为了获得速度,滑的区域必须向下倾斜,而且当你滑到坡底,你不得不再次走上坡或者等待升降机来载你。Michael想知道在一个区域中最长的滑坡。区域由一个二维数组给出。数组的每个数字代表点的高度。下面是一个例子:
1   2   3   4   5
16 17 18 19 6
15 24 25 20 7
14 23 22 21 8
13 12 11 10 9

【问题分析】

按照高度,从大到小排列,然后利用动态规划往一个高度下降的方向就可以处理,转换为类似于最长上升子序列问题:
F[i]=max(f[j])+1(a[i]<a[j])

例题:时间轴动归-Tom的烦恼

【问题描述】
Tom加工一些不同零件,不同零件的加工费和加工时间要求不同,有些加工时间要求甚至是冲突的(但开始和结束时间相同不算冲突)在某个时间内他只能选择某种零件加工(因为他只有一台机器),为了赚得尽量多的加工费,Tom不知如何进行取舍,现在请你帮Tom设计一个程序,合理选择部分(或全部)零件进行加工,使得得到最大的加工费。
输入文件input.txt的第一行是一个整数n表示共有n个零件须加工。接下来的n行中,每行有3个整数,分别表示每个零件加工的时间要求,第一个表示开始时间,第二个表示该零件加工的结束时间,第三个表示加工该零件可以得到的加工费。(数据中的每个数值不会超过100000)
输出文件output.txt只包含一个整数,表示Tom可以得到的最大加工费。结果输出到文件output.txt。
【输入样例】
3
1 3 10
4 6 20
2 5 25
【输出样例】
30

【问题分析】

用sum[i]表示到达时刻i时所能得到的最大收益,用a[j,1]表示任务j的开始时间,a[j,2]表示任务j的结束时刻,b[j]表示任务j完成所得的加工费。状态转移方程:sum[i]=max{sum[k]]+b[j] | 1<=k<=a[j,1]<a[j,2]<=i}

sum[0]:=0;
for i:=1 to m do
    begin
    max:=0;
    for j:=1 to n do
        if a[j,2]<=i
            then if max<sum[a[j,1]]+b[j]
                then max:=sum[a[j,1]]+b[j];
    sum[i]:=max;
    end;

【算法优化】

1、原算法的时间复杂度是?
2、是否有优化的余地?
3、排除重复是本题一个优化的方向

对所有任务按照结束时间进行从小到大排序;
计算最后一个任务的结束时刻m;
ans[0]:=0;
for i:=1 to m do
    begin
        ans[i]:=ans[i-1];
        if 当前有任务j刚好结束(j可能不止一个) then
        begin
            if ans[i]<ans[a[j,1]]+b[j] 
                then ans[i]:= ans[a[j,1]]+b[j];
        end;
    end;

例题:多进程动态规划-传纸条

【问题描述-noip2008】
小渊和小轩被安排在m行n列矩阵对角线的两端,因此,他们无法直接交谈了。幸运的是,他们可以通过传纸条来进行交流。从小渊传到小轩的纸条只可以向下或者向右传递,从小轩传给小渊的纸条只可以向上或者向左传递。班里每个同学都可以帮他们传递,但只会帮他们一次。
还有一件事情需要注意,全班每个同学愿意帮忙的好感度有高有低,可以用一个0-100的自然数来表示,数越大表示越好心。小渊和小轩希望尽可能找好心程度高的同学来帮忙传纸条,即找到来回两条传递路径,使得这两条路径上同学的好心程度之和最大。输出最大的好心程度之和。

【问题分析】

问题简化:假定小渊传给小轩,小轩无需回复。

数字三角形:F[i][j]=max(f[i-1][j-1]+f[i-1][j])+a[i][j]

两条路:

加一维:F[i][j][k]=max(f[i-1][j-1][k-1], f[i-1][j-1][k],f[i-1][j][k-1], f[i-1][j][k])+a[i][j]+a[i][k],对吗?

加判断:F[i][j][k]=max(f[i-1][j-1][k-1], f[i-1][j-1][k],f[i-1][j][k-1], f[i-1][j][k])+a[i][j]+a[i][k](j!=k && 不能从上一行的同一个格子转移)

猜你喜欢

转载自blog.csdn.net/u013228808/article/details/83239168
今日推荐