动态规划总结

引子:带权有向的多段图问题

      给定一个带权的有向图,要求从点A到点D的最短路径。

        

     设F(i)表示从点A到达点i的最短距离,则有

         F(A)=0

         F(B1)=5,F(B2)=2

         F(C1)=min{F(B1)+3}=8

         F(C2)=min{F(B1)+2,F(B2)+7}=7

         F(C3)=min{F(B2)+4}=6

         F(D)=min{F(C1)+4,F(C2)+3,F(C3)+5}=10。

多阶段最优化决策问题

    由上例可以看出,整个问题分成了A、B、C、D四个阶段来做,每个阶段的数值的计算只会跟上一个    阶段的数 值相关,这样一直递推下去直到目标。

    即由初始状态开始,通过对中间阶段决策的选择,达到结束状态。这些决策形成了一个决策序列,同时确定了完成整个过程的一条最优的活动路线。

    

状态转移方程

    设fk(i)—k阶段状态i的最优权值,即初始状态至状态i的最优代价。

       fk+1(i) = min{ fk(j) + u(i,j) }

动态规划的基本原理

    1.最优性原理:作为整个过程的最优策略,它满足:相对前面决策所形成的状态而言,余下的子策略必然构成“最优子策略”。

    2.无后效性原则:给定某一阶段的状态,则在这一阶段以后过程的发展不受这阶段以前各段状态的影响,所有各阶段都确定时,整个过程也就确定了。这个性质意味着过程的历史只能通过当前的状态去影响它的未来的发展,这个性质称为无后效性。



!!!下面真题见真招:

一.关键子工程

   1.有N个子工程,每一个子工程都有一个完成时间.

   2.子工程之间的有一些依赖关系,即某些子工程必须在一些子工程完成之后才开工。

   3.在满足子工程间的依赖关系前提下,可以有任何多个子工程同时在施工。

   4.求整个工程的完成的最短时间。

   5.求出所有关键子工程。

   6.N≤200

   提出一个概念:有向图的关键路径

      

  分析:1.如果该图能够进行拓扑排序,证明有解,反之则无解。

        2.根据拓扑序列进行动态规划求解,得到工程所需的完成时间。

          设F[I]表示完成子工程I所需的最早时间。

          动态规划方程:F[I]=MAX{F[J]}+ A[I,J]

        3.根据的得到的F序列和拓扑序列,查找关键工程。初始时,最后完成的一个或多个工程为关键工程。

         如果F[I]=F[J]-A[I,J] ,且第I个子工程为关键工程,那么第J个子工程也是关键工程。

        4.时间复杂度为O(N^2)。

 【代码】

program project(input,output);

const

   maxn    = 200;

var

   r         : array[1..maxn,1..maxn]of longint;

   s,d,t,f     : array[1..maxn]of longint;

   k         : array[1..maxn]of boolean;

   ans,tot,i,j,n : longint;

procedure print(ok: boolean);{输出答案}

var

   i : longint;

begin

   assign(output,'project.out');rewrite(output);

   if ok then

   begin

      writeln(ans);

      for i:=1 to n do

     if k[i] then

        write(i,' ');

      writeln;

   end

   else

      writeln(-1);

   close(output);

   halt;

end;

begin

   assign(input,'project.in');reset(input);

   readln(n);

   for i:=1 to n do{读入每个工程需要的时间}

      read(t[i]);

   for i:=1 to n do{读入依赖关系}

      for j:=1 to n do

     if i<>j then

        read(r[i,j]);

   close(input);

  

   for i:=1 to n do{统计每个工程所依赖的工程数目}

      for j:=1 to n do

         inc(d[i],r[i,j]);

  

   tot:=0;{按照依赖关系拓扑排序}

   for i:=1 to n do{找出不依赖任何工程的工程}

      if d[i]=0 then

      begin

        inc(tot);

        s[tot]:=i;

      end;

   i:=0;ans:=0;

   repeat

      inc(i);

      for j:=1 to n do{因为该工程依赖的工程都已经找出,所以可以计算该工程最早开工时间}

     if r[s[i],j]=1 then

        if f[j]>f[s[i]] then

           f[s[i]]:=f[j];

     

     inc(f[s[i]],t[s[i]]);{计算该工程最早结束时间}


     if f[s[i]]>ans then{更新答案}

      ans:=f[s[i]];

     

     for j:=1 to n do{判断是否有新的工程可以开工}

      if d[j]<>0 then

      begin

         dec(d[j],r[j,s[i]]);

         if d[j]=0 then

         begin

            inc(tot);

            s[tot]:=j;

         end;

      end;

  until i=tot;

  

  if tot<>n then{如果不存在拓扑序列}

      print(false);

  

  for i:=tot downto 1 do{寻找关键工程}

  begin

    if f[s[i]]=ans then{最晚完成的一定是关键工程}

     k[s[i]]:=true;

    for j:=1 to n do

     if (r[j,s[i]]=1)and(k[j])and(f[s[i]]+t[j]=f[j]) then{直接影响到关键工程的工程也是关键工程}

        k[s[i]]:=true;

  end;

  print(true);

end.

二.机器分配

   1.M台设备,分给N个公司。

   2.若公司i获得j台设备,则能产生A(i,j)效益。

   3.问如何分配设备使得总效益最大?

   分析:1.用机器数来做状态,数组F[I,J]表示前I个公司分配J台机器的最大盈利。则状态转移方程为:

           F[I,J]=Max{F[I-1,K] + A[I,J-K]} (1<=I<=N,1<=J<=M,0<=K<=J )

         2.初始值: F(0,0)=0。

         3.时间复杂度O(N*M^2)。

 【代码】

program machine(input,output);

const

   maxn=100;

var

   f,v       :array[0..maxn,0..maxn]of longint;{注意数组要开到0}

   i,j,k,m,n :longint;

begin

   readln(m,n);

   for i:=1 to n do{读入参数}

      for j:=1 to m do

     read(v[i,j]);

   for i:=1 to n do

      for j:=0 to m do{前i个公司分配j台机器的最大盈利}

     for k:=0 to j do{如果分配给i公司k台机器}

        if f[i-1,j-k]+v[i,k]>f[i,j] then

           f[i,j]:=f[i-1,j-k]+v[i,k];

   writeln(f[n,m]);

end.

三.编辑距离

   1.给定两个字符串A和B。

   2.给定三种操作:

         –插入一个字符。

         –删除一个字符。

         –修改某个字符。

   3.求使得A=B的最少操作次数。

   4.A、B的长度不超过1000。

   分析:1.设F[i,j]表示从初始串前i位与目标串前j位相等的最少操作次数,则状态转移方程:

         f[i,j]=min{f[i-1,j]+1 (i>=1)  删除初始串中的第i个字符。

                    f[i,j-1]+1 (j>=1)  在初始串的第i个字符末尾添加一个字符。

                    f[i-1,j-1]+1 (i>=1,j>=1,a[i]<>b[j]) 将初始串的第i个字符变换成目标串的第j个字符

                    f[i-1,j-1]  (i>=1,j>=1.a[i]=b[j]) 初始串的第i个字符与目标串的第j个字符相同.}

【代码】

program edit(input,output);

const

   maxn=1000;

var

   f         : array[0..maxn,0..maxn]of longint;

   a,b       : ansistring;

   i,j,n,m   : longint;

function min(x,y: longint):longint;

begin

   if x<y then

      exit(x)

   else

      exit(y);

end;

begin

   assign(input,'edit.in');reset(input);

   readln(a);

   readln(b);

   n:=length(a);

   m:=length(b);

   close(Input);

   for i:=0 to n do

      for j:=0 to m do{把a串的前i位修改成b串的前j位需要的操作数}

     if (i<>0)or(j<>0) then{边界条件f[0,0]=0,不需要转移}

     begin

        f[i,j]:=maxlongint;{因为是求最小,初始化要无穷大}

        if i<>0 then

           f[i,j]:=min(f[i,j],f[i-1,j]+1);{a串增加一个字符b[j]的情况}

        if j<>0 then

           f[i,j]:=min(f[i,j],f[i,j-1]+1);{a串删除第j个字符的情况}

        if (i<>0)and(j<>0) then

           f[i,j]:=min(f[i,j],f[i-1,j-1]+ord(a[i]<>b[j]));{a串修改一个字符或者不用修改的情况}

     end;

   assign(output,'edit.out');rewrite(output);

   writeln(f[n,m]);

   close(output);

end.

四.硬币找零

      1.给定N枚硬币,给定T元钱。

      2.用这N枚硬币找这T元钱,使得剩下的钱最少。

      3.剩下钱数最少的找零方案中的所需的最少硬币数。

      4.N<=500,T<=10000.

      分析:1.设F[i]表示需要找的钱数为i时所需要的最少钱币数。显然有:

               F[i]=Min{F[ i -A[j] ] + 1} { i≤ T,1≤j≤N}

            2.初始值:F[0]=0。

              A[j]表示其中第j种钱币的面值。

            3.时间复杂度为O(N*T)。

【代码】

program coin(input,output);

const

   maxt=100000;

   maxn=50;

var

   v         : array[1..maxn]of longint;

   f         : array[0..maxt]of longint;

   t,i,j,m,n : longint;

begin

   assign(input,'coin.in');reset(input);

   readln(n,t);

   for i:=1 to n do

      read(v[i]);

   close(input);

   for i:=1 to t do{f[i]表示拼出i至少需要多少枚硬币}

   begin

      f[i]:=maxt;{因为求最少硬币数,所以初始化无穷大}

      for j:=1 to n do{枚举用哪枚硬币}

     if i>=v[j] then

        if f[i-v[j]]+1<f[i] then

           f[i]:=f[i-v[j]]+1;

   end;

   assign(output,'coin.out');rewrite(output);

   for i:=t downto 0 do

      if f[i]<>maxt then

      begin

     writeln(f[i]);

     break;

      end;

   close(output);

end.

五.拦截导弹

    1.给定N个数,求最长的不上升子序列长度,求最少有多少个不上升序列能覆盖所有的数,即求最少覆盖序列。

    2.N<=10000.

    分析:1.设f(i)表示前i个数的最长不上升序列的长度。则:

               f(i)=max{f(j)+1},其中j<i and a[j]>=a[i]  这里0<j<i<=n 显然时间复杂度为O(n2)。

          2.上述式子的含义:找到i之前的某j,这个数不比第i个数小,对于所有的j取f(j)的最大值。

    优化:分析样例,

          这里找j,是在1~i之间进行寻找,那么我们能否快速查找到我们所要更改的j呢?

          要能更改需要两个条件:①j<i and a[j]>=a[i]。

                                ②f(j)尽可能大。

          以上两个条件提示我们后面的值一定要小于等于前面的值。因此我们试着构建一个下降的序列。在

          这个下降的序列中查找可以更改的f值,使得序列的值尽可能大。

具体过程:

i          1          2          3          4          5          6          7          8

          389        207        155        300        299        170        158         65

第1次     389

第2次     389        207

第3次     389        207        155

第4次     389        300        155         (由于207<300<389,因此更新)

第5次     389        300        299         (由于155<299<300,因此更新)

第6次     389        300        299        170

第7次     389        300        299        170        158

第8次     389        300        299        170        158         65

有些同学可能会问:

    对于每个f,为什么只保留一个数值呢?

    而对于该序列,为什么要保留较大的值呢?

    1.再回过头来看方程:

            f(i)=max{f(j)+1},其中j<iand a[j]>=a[i]

      该式子表示找前面的一个最大f的符合条件的j,因此只要保存符合条件的最大的j就可以了。

    2.在f值相同的情况下,保留较大的数显然更好。因为后面的数若能跟较小的数构成下降序列也一定能与

      较大的数构成下降序列,反之则不一定。例如207与300的f=2,但207不能与299构成下降序列,而300则可

      以。

    3.因为生成的序列为有序序列,因此我们可以采用二分查找的方法很快查找到更新的值,时间复杂度为

      O(n㏒n)

求导弹的最小覆盖:

    1.第二问很容易想到贪心法:那就是采取多次求最长不上升序列的办法,然后得出总次数。

      上述贪心法不正确,很容易就能举出反例。

        例如:“7 5 4 1 6 3 2”用多次求最长不上升序列所有为“7 5 4 3 2”、“1”、“6”共3套系统;

       但其实只要2套,分别为:“7 5 4 1”与“6 3 2”。

    上述问题的原因是若干次最优决策值和不一定能推导出整个问题的最优。

    2.为了解决这个问题下面介绍一个重要性质:  –最少链划分=最长反链长度

      为了证明这个性质,需要了解离散数学中偏序集的相关概念和性质以及偏序集的Dilworth定理。

 偏序集:

       偏序是在集合X上的二元关系≤(这只是个抽象符号,不是“小于或等于”),它满足自反性、反对称

   性和传递性。即,对于X中的任意元素a,b和c,有:

        –自反性:a≤a;

        –反对称性:如果a≤b且b≤a,则有a=b;

        –传递性:如果a≤b且b≤c,则a≤c 。

       带有偏序关系的集合称为偏序集。

       令(X,≤)是一个偏序集,对于集合中的两个元素a、b,如果有a≤b或者b≤a,则称a和b是可比的,否则a

   和b不可比。

       一个反链A是X的一个子集,它的任意两个元素都不能进行比较。

       一个链C是X的一个子集,它的任意两个元素都可比。

       在X中,对于元素a,如果任意元素b,都有a≤b,则称a为极小元。

       定理1:

         令(X,≤)是一个有限偏序集,并令r是其最大链的大小。则X可以被划分成r个但不能再少的反链。

       Dilworth定理(其对偶定理):

         令(X,≤)是一个有限偏序集,并令m是反链的最大的大小。则X可以被划分成m个但不能再少的链。

       虽然这两个定理内容相似,但第一个定理证明要简单一些。此处就只证明定理1。

       证明:

          设p为最少反链个数,

          (1)先证明X不能划分成小于r个反链。由于r是最大链C的大小,C中任两个元素都可比,因此C中任两

          个元素都不能属于同一反链。所以p>=r。

          (2)设X1=X,A1是X1中的极小元的集合。从X1中删除A1得到X2。注意到对于X2中任意元素a2,必存在

          X1中的元素a1,使得a1<=a2。令A2是X2中极小元的集合,从X2中删除A2得到X3……,最终会有一个Xk

          非空而Xk+1为空。于是A1,A2,…,Ak就是X的反链的划分,同时存在链a1<=a2<=…<=ak,其中ai在Ai

          内。由于r是最长链大小,因此r>=k。由于X被划分成了k个反链,因此r>=k>=p。

          (3)因此r=p,定理1得证。

  解决:

     1.要求最少的覆盖,按照Dilworth定理:最少链划分= 最长反链长度所以最少多少套系统= 最长导弹高度上

     升序列长度。

     2.知道了怎样求最长不上升算法,同样也就知道了怎样求最长上升序列。

     3.时间复杂度O(n㏒n)。

【代码】

program missile(input,output);

const

   maxn=100000;

   max=maxlongint;

var

   len,h:array[0..maxn]of longint;

   now,ans1,ans2,i,n,m,l,r,c:longint;

begin

   assign(input,'missile.in');reset(input);

   readln(n);

   for i:=1 to n do

      read(h[i]);

   close(input);


   {求第一问}


   len[0]:=max;{初始化}

   for i:=1 to n do

      len[i]:=0;h

   ans1:=0;


   for i:=1 to n do

   begin

      l:=0;r:=n;{二分找一个可以拦截第i枚导弹的最大长度}

      repeat

     m:=(l+r) shr 1;

     if len[m]>=h[i] then

     begin

        l:=m+1;

        c:=m;

     end

     else

        r:=m-1;

      until l>r;

      now:=c+1;{更新当前长度为now的导弹高度}

      if h[i]>len[now] then

     len[now]:=h[i];

      if now>ans1 then{更新答案}

     ans1:=now;

   end;


   {求第二问:最少链划分=最长反链长度,所以最少多少套系统等于最长导弹高度上升序列长度}


   len[0]:=0;{初始化}

   for i:=1 to n do

      len[i]:=max;

   ans2:=0;


   for i:=1 to n do

   begin

      l:=0;r:=n;{二分找一个比第i枚导弹高度低的最大长度}

      repeat

        m:=(l+r) shr 1;

        if len[m]<h[i] then

        begin

          l:=m+1;

          c:=m;

        end

        else

          r:=m-1;

      until l>r;

      now:=c+1;{更新当前长度为now的导弹高度}

      if h[i]<len[now] then

        len[now]:=h[i];

      if now>ans2 then{更新答案}

        ans2:=now;

   end;

   assign(output,'missile.out');rewrite(output);

   writeln(ans1);

   writeln(ans2);

   close(output);

end.

六.整数划分

      1.给出一个长度为n的数

      2.要在其中加m-1个乘号,分成m段

      3.这m段的乘积之和最大

      4.m<n<=20

      5.有T组数据,T<=10000

   贪心法:1.尽可能平均分配各段,这样最终的数值将会尽可能大。但有反例。

            如191919分成3段 19*19*19=6859

            但191*91*9=156429,显然乘积更大。

           2.将一个数分成若干段乘积后比该数小,因为输入数不超过20位,因此不需高精度运算。

             证明:

                  –假设AB分成A和B,且A,B<10,则有

                  –AB=10*A+B>A*B(相当于B个A相加)

                  –同理可证明A,B为任意位也成立

   动态规划:1.可以先预处理出原数第i到j段的数值A[i,j]是多少,这样转移就方便了,预处理也要尽量降低

               复杂度。

             2.F[i,j]表示把这个数前i位分成j段得到的最大乘积。

               F[i,j]=F[k,j-1]*A[k+1,i],   1<k<i<=n, j<=m

             3.时间复杂度为O(mn^2)

             4.由于有10000组数据,因此估计时间复杂度为10000*203=8*107

             5.至于说输出,记录转移的父亲就可以了。

【代码】

program separate(input,output);

const

   maxn=20;

var

   v,f           : array[0..maxn,0..maxn]of int64;

   g           : array[0..maxn,0..maxn]of longint;

   t,m,i,j,k,l : longint;

   n,sum       : int64;

   s           : string;

function min(a,b: longint):longint;{取a和b中的小值}

begin

   if a<b then

      min:=a

   else

      min:=b;

end;

procedure print(l,m:longint);{因为动态规划是正向的,所以决策点是逆向的,可以采用递归正向输出}

var

   i : longint;

begin

   if m=0 then exit;

   print(g[l,m],m-1);

   for i:=g[l,m]+1 to l do

      write(s[i]);

   write(' ');

end;

begin

   assign(input,'separate.in');reset(input);

   assign(output,'separate.out');rewrite(output);

   readln(t);

   for t:=1 to t do

   begin

      readln(n,m);

      str(n,s);

      l:=length(s);

      for i:=1 to l do{预处理出n第i到j位的数值}

      begin

     sum:=0;

     for j:=i to l do

     begin

        sum:=sum*10+ord(s[j])-48;

        v[i,j]:=sum;

     end;

      end;

      for i:=0 to l do{初始化,答案有可能是0,所以要处理成负的}

     for j:=0 to l do

        f[i,j]:=-1;

      f[0,0]:=1;

      for i:=1 to l do{f[i,j]表示将n的前i位分成j分能获得的最大值}

     for j:=1 to min(i,m) do

        for k:=1 to i do{枚举上一个状态进行转移}

           if f[k-1,j-1]*v[k,i]>f[i,j] then

           begin

          f[i,j]:=f[k-1,j-1]*v[k,i];

          g[i,j]:=k-1;{记录决策点}

           end;

      writeln(f[l,m]);

      print(l,m);

      writeln;

   end;

   close(input);

   close(output);

end.

七.快餐问题

       1.套餐由由A个汉堡,B个薯条和C个饮料组成

       2.生产汉堡、薯条、饮料的时间为p1,p2,p3

       3.有N条流水线,N<=10,第i条流水线生产时间为Ti

       4.每条流水线都可生产汉堡、薯条和饮料

       5.每天汉堡、薯条和饮料个数不超过100

       6.如何安排生产使得生产套餐数量最多。

  分析:1.对于每条流水线,它的生产时间是一定的,我们如果知道了生产汉堡和薯条的个数,就很容易计算出

          生产饮料的个数。因此:

          假设第i条流水线生产了i’个汉堡,j’个薯条,那么还能生产饮料个数为:

            k’=(Ti-i’*p1-j’*p2)/p3

          

        2.设f(x,i,j)表示前x条流水线生产i个汉堡,j个薯条最多还能生产饮料个数。

          如果前x-1条流水线只要生产i’汉堡,j’个薯条,生产的饮料为f(x-1,i’,j’)

          因此有第x条流水线必须生产了i-i’个汉堡,j-j’个薯条,剩下的时间生产k个饮料。

          k=(Tx-(i-i')*p1-(j-j')*p2)/p3

          因为前x条流水线生产的饮料=前x-1条流水线生产的饮料+ 第x条流水线生产的饮料

          所以有,f(x,i,j)=max{f(x-1,i’,j’)+k}

    流水线资源分配图:

        

        其中 k=(Tx-(i-i')*p1-(j-j')*p2)/p3。

   !!但是

        f(x,i,j)=max{f(x-1,i’,j’)+k}    1<=x<=n<=10,0<=i’<=i<=100, 0<=j’<=j<=100

        k可以实时求出

        时间复杂度为10*1002*1002=10^9    显然超时!

    优化:

       1.求出最多可能生产多少套,减少不必要循环。

       2.淘汰一些没有意义状态,比如生产了过多第三种物质,却没把前两种生产满。

       3.让每条流水线大多数时间按成套时间生产,留下一些时间进行动态规划,这样将在动态规划时,极大

         地减少每种物品的产量从而极大地减少动态规划的状态数。

    说明:

       1.利用前两个优化基本可以出解

       2.第3个优化,对每条流水线进行动态规划的剩余时间多少比较难确定,否则正确性难以保证,剩余时间

         越多,正确率越高,速度越慢。下图是对第3个优化的说明:

         

    总结:

      第1步:采用优化方法3,将每条流水线的部分时间按成套生产,设第i条流水线生产套餐Wi套,剩下的时

             间为Ti;

      第2步,对每条流水线的剩下时间进行动态规划,并采用优化1,2;

      第3步,求总套数= 贪心总套数X+ 动态规划的套数Y。

       其中X,Y计算如下,

        x=Σ(1..n)w[i]

        y=max{min{i/a,j/b,f(n,i,j)/c}}

       随所有i,j进行枚举。

【代码】

 program meal(input,output);

const

   maxn=10;

   maxt=100;

var

   f                         : array[0..maxn,0..maxt,0..maxt]of longint;

   t                         : array[1..maxn]of longint;

   sum,tot,bas,now,ans,maxans,maxa,maxb,maxc : longint;

   i,j,k,x,y,z,a,b,c,p1,p2,p3,n             : longint;

function min(a,b                 : longint):longint;{取小值}

begin

   if a<b then

      min:=a

   else

      min:=b;

end;

function check(i,j,k : longint):boolean;{检查当前状态是否有意义}

begin

   if f[i,j,k]=-1 then

      exit(false);

   if f[i,j,k]>maxc then{如果前两种没造满却把第三种造太多,显然没意义}

   begin

      if (j<>maxa)and((f[i,j,k]-maxc)*p3>p1) then

     exit(false);

      if (k<>maxb)and((f[i,j,k]-maxc)*p3>p2) then

     exit(false);

   end;

   check:=true;

end;

begin

   assign(input,'meal.in');reset(input);

   readln(a,b,c);

   readln(p1,p2,p3);

   readln(n);

   for i:=1 to n do

      read(t[i]);

   close(input);

  

   maxans:=min(maxt div a,maxt div b);{预算最大可能答案}

   maxans:=min(maxans,maxt div c);

  

   tot:=a*p1+b*p2+c*p3;{贪心每条流水线先整套生产}

   for i:=1 to n do

      while (t[i]>=3*tot)and(bas<maxans) do

      begin

     dec(t[i],tot);

     inc(bas);

      end;

   dec(maxans,bas);

  

   sum:=0;{继续更新答案最大可能值}

   for i:=1 to n do

      inc(sum,t[i]);

   maxans:=min(maxans,sum div tot);

   maxa:=maxans*a;

   maxb:=maxans*b;

   maxc:=maxans*c;

  


   fillchar(f,sizeof(f),255);

   f[0,0,0]:=0;{f[i,j,k]表示用了前i条流水线,生产了j个第一种产品,k个第二种产品,最多能生产多少个第三种产品}

   for i:=0 to n-1 do

      for j:=0 to maxa do

     for k:=0 to maxb do

        if check(i,j,k) then{检查状态是否有意义}

           for x:=0 to min(maxa-j,t[i+1] div p1) do

          for y:=0 to min(maxb-k,(t[i+1]-p1*x)div p2) do

          begin

             z:=(t[i+1]-p1*x-p2*y)div p3;

             if f[i,j,k]+z>f[i+1,j+x,k+y] then

            f[i+1,j+x,k+y]:=f[i,j,k]+z;

          end;

   for j:=0 to maxt do{寻找答案}

      for k:=0 to maxt do

     if f[n,j,k]<>-1 then

     begin

        now:=min(j div a,k div b);

        now:=min(now,f[n,j,k] div c);

        if now>ans then

           ans:=now;

     end;

   assign(output,'meal.out');rewrite(output);

   writeln(ans+bas);

   close(output);

end.

八.物品装箱问题

       1.N件物品

           –编号:A1、A2、…、An

           –重量:Aw1、Aw2、…、Awn

           –价值:Av1、Av2、…、Avn

       2.N件代用件

           –编号:B1、B2、…、Bn

           –重量:Bw1、Bw2、…、Bwn

           –价值:Bv1、Bv2、…、Bvn

       3.装填的第I步,要么装入Ai,要么装入Bi,要么Ai和Bi都不装。

       4.装载tot重量,使得总价值最大。

   分析:1.这道题是背包问题的加强版本,首先我们看一般的背包问题如何解决。

         2.一般背包问题:

             –N件物品,第i件物品重量为Wi,价值Vi,

             –如何选取物品放入承重W的背包,使得总价值最大。

             设前i种货物占用了j重量资源后所能得到的最大价值,显然,对于第i种货物有装载和不装载两种

             情况。因此:

              f[i,j]=max{f[i-1,j]  不装载

                         f[i-1,j-v[i]]+w[i] 装载}

             时间复杂度为O(nW)。

         3.对于本题,因为A和B只能取一个,所以只要同时进行决策就可以了,这样就能避免同时选的情况。

         4.设前i种货物占用了j重量资源后所能得到的最大价值,显然,对于第i种货物有分别对A、B两种物质

           装载和不装载两种情况。因此:

           f[i,j]=max{f[i-1,j]

                      f[i-1,j-Va[i]]+Wa[i]

                      f[i-1,j-Vb[i]]+Wb[i] }

           约束:1<=i<=n,0<=j<=TOT,初始:F[i,0]=0

           目标:Max{F[n,TOT]}。时间复杂度为O(n*TOT).

九.火车进站

       1.给定N辆火车

            –第i辆火车的进站时间arrive(i)

            –第i辆火车的离站时间leave(i)

       2.车站只能容纳M辆火车

       3.求最多能接受多少辆火车?

       4.M<=3

    先看样例:

      

    1.第1,2,3辆分别进入(1 2 3 );

    2.第2辆离开,可以看出要离开时,被第1辆火车卡在前面,因此第1辆火车不能进入,队列为(2 3)

    3.第2辆离开,第4辆进入(3 4)

    4.第3,4辆离开,队列空

    5.第5,6辆进入(5 6)

    6.第5,6分别离开,队列空

    因此答案为5辆。

    分析:

       1.按到达时间排序和离开时间排序,这样每一辆火车用线段描述,有:

          –排在前面的火车,其进站时间必须先于排在后面的火车;

          –排在前面的火车,其出站时间必须先于排在后面的火车,否则该列火车就要先进后出,不满足列 

            特点。

       2.这样对于任一列排序后的火车i,只有排在其后的火车才有可能在它出站之后进站。接下来的任务便是

         采用动态规划方法求解了。

   当m=1 时

         

         设f[i]表示第i 列火车进站时,其后的火车最多可以进站的数量,则有:

            f[i] =max{f[j]+1},(满足i比j先进站,且j在i 出站之后进站);

   当m=2 时

         

         设f[i,j]表示车站停靠i,j 两列火车(i<j)时,其后的火车(包括i,j本身)最多可以进站的数量,则:

             f[i,j]=max{f[j,k]+1}

         条件:必须满足按i,j,k顺序进站和出站,另外还要满足k在i出站后且j 进站。

   当m=3 时

          

          设f[i,j,k]表示车站停靠i,j,k三列火车(i<j<k)时,其后的火车(包括i,j,k)最多可以进站的数

          量。则有,

              f[i,j,k]=max{f[j,k,l]+1}

          条件:必须满足按i,j,k,l顺序进站和出站,另外还要满足l在i 出站后进站。

【代码】

program train;

const

   maxn=100;

var

   f1         : array[0..maxn]of longint;

   f2         : array[-1..maxn,-1..maxn]of longint;

   f3         : array[-2..maxn,-2..maxn,-2..maxn]of longint;

   t         : array[-2..maxn,1..2]of longint;

   i,j,k,m,n : longint;

procedure find1(p1: longint);

var

   p2 : longint;

begin

   if f1[p1]<>0 then{f1[p1]表示当前站内正在停p1号车时,最多还能有多少车进站}

      exit;

   f1[p1]:=1;

   for p2:=p1+1 to n do{枚举下一辆车}

      if t[p2,1]>=t[p1,2] then

      begin

     find1(p2);

     if f1[p2]+1>f1[p1] then

        f1[p1]:=f1[p2]+1;

      end;

end;

procedure find2(p1,p2:longint);

var

   p3 : longint;

begin

   if f2[p1,p2]<>0 then{f2[p1,p2]表示当前站内正在停p1,p2号车时,最多还能有多少车进站}

      exit;

   f2[p1,p2]:=1;

   for p3:=p2+1 to n do{枚举下一辆车}

      if (t[p3,2]>=t[p2,2])and(t[p3,1]>=t[p1,2]) then

      begin

     find2(p2,p3);

     if f2[p2,p3]+1>f2[p1,p2] then

        f2[p1,p2]:=f2[p2,p3]+1;

      end;

end;

procedure find3(p1,p2,p3:longint);

var

   p4 : longint;

begin

   if f3[p1,p2,p3]<>0 then{f3[p1,p2,p3]表示当前站内正在停p1,p2,p3号车时,最多还能有多少车进站}

      exit;

   f3[p1,p2,p3]:=1;

   for p4:=p3+1 to n do{枚举下一辆车}

      if (t[p4,2]>=t[p3,2])and(t[p4,1]>=t[p1,2]) then

      begin

     find3(p2,p3,p4);

     if f3[p2,p3,p4]+1>f3[p1,p2,p3] then

        f3[p1,p2,p3]:=f3[p2,p3,p4]+1;

      end;

end;

begin

   assign(input,'train.in');reset(input);

   readln(n,m);

   for i:=1 to n do

      readln(t[i,1],t[i,2]);

   close(input);


   for i:=1 to n do{按进站时间排序,其次按出站时间排序,这样动规可以得到最大值}

      for j:=i+1 to n do

     if (t[i,1]>t[j,1])or(t[i,1]=t[j,1])and(t[i,2]>t[j,2]) then

     begin

        t[0]:=t[i];t[i]:=t[j];t[j]:=t[0];

     end;


   for i:=-2 to 0 do{边界条件}

      for j:=1 to 2 do

     t[i,j]:=t[1,1]-1;


   assign(output,'train.out');rewrite(output);

   case m of{对m分情况讨论,因为状态可能不多,所以采用记忆化搜索比较合适}

     1:begin

      find1(0);

      writeln(f1[0]-1);

       end;

     2:begin

      find2(-1,0);

      writeln(f2[-1,0]-1);

       end;

     3:begin

      find3(-2,-1,0);

      writeln(f3[-2,-1,0]-1);

       end;

   end;

   close(output);

end.

十.凸多边形的三角剖分

         1.给定由N顶点组成的凸多边形

         2.每个顶点具有权值

         3.将凸N边形剖分成N-2个三角形

         4.求N-2个三角形顶点权值乘积之和最小?

     样例

          

         上述凸五边形分成△123 ,△135,△345

          三角形顶点权值乘积之和为:121*122*123+121*123*231+123*245*231=12214884

     分析:

        1.性质:一个凸多边形剖分一个三角形后,可以将凸多边形剖分成三个部分:

          一个三角形+二个凸多边形(图2可以看成另一个凸多边形为0)

         

     动态规划:

         1.如果我们按顺时针将顶点编号,则可以相邻两个顶点描述一个凸多边形。

         2.设f(i,j)表示i~j这一段连续顶点的多边形划分后最小乘积

         3.枚举点k,i、j和k相连成基本三角形,并把原多边形划分成两个子多边形,则有

               f(i,j)=min{f(i,k)+f(k,j)+a[i]*a[j]*a[k]}   1<=i<k<j<=n

           时间复杂度O(n^3)

    为什么可以不考虑这种情况?

        

     

  

     可以看出图1和图2是等价的,也就是说如果存在图1的剖分方案,则可以转化成图2的剖分方案,因此可以

     不考虑图1的这种情形。

【代码】

program division(input,output);

const

   maxn=50;

   max=32767*32767*32767;

var

   f       : array[1..maxn,1..maxn]of int64;

   v       : array[1..maxn]of int64;

   i,j,k,n : longint;


begin

   assign(input,'division.in');reset(input);

   readln(n);

   for i:=1 to n do

      read(v[i]);

   close(input);


   for i:=n-2 downto 1 do {f[i,j]表示i~j这一段连续顶点的多边形划分后最小乘积}

      for j:=i+2 to n do

      begin

     f[i,j]:=max;{因为是求最小,初始化无穷大}

     for k:=i+1 to j-1 do{枚举点k,i、j和k相连成基本三角形,并把原多边形划分成两个子多边形}

        if v[i]*v[j]*v[k]+f[i,k]+f[k,j]<f[i,j] then

           f[i,j]:=v[i]*v[j]*v[k]+f[i,k]+f[k,j];

      end;


   assign(output,'division.out');rewrite(output);

   writeln(f[1,n]);

   close(output);

end.

十一.加分二叉树

     1.给定一个中序遍历为1,2,3,…,n的二叉树

     2.每个结点有一个权值

     3.定义二叉树的加分规则为:

                    –左子树的加分×右子树的加分+根的分数

                    –若某个树缺少左子树或右子树,规定缺少的子树加分为1。

     4.构造符合条件的二叉树

                     –该树加分最大

                     –输出其前序遍历序列

     样例:

       中序遍历为1,2,3,4,5的二叉树有很多,下图是其中的三棵,其中第三棵加分最大,为145.

       

     分析:

       1.性质:中序遍历是按“左-根-右”方式进行遍历二叉树,因此二叉树左孩子遍历序列一定在根结点的

       左边,右孩子遍历序列一定在根结点的右边!

       2.因此,假设二叉树的根结点为k,那么中序遍历为1,2,…,n的遍历序列,左孩子序列为1,2,…,k-1,右

       孩子序列为k+1,k+2,…,n,如下图

       

     动态规划:

        1.设f(i,j)中序遍历为i,i+1,…,j的二叉树的最大加分,则有:

            f(i,j)=max{f[i,k-1]*f[k+1,j] +d[k]}  1<=i<=k=<=j<=n

            显然f(i,i)=d[i]

            答案为f(1,n)

            时间复杂度O(n^3)

        2. 要构造这个树,只需记录每次的决策值,令b(i,j)=k,表示中序遍历为i,i+1,…,j的二叉树的取最

           优决策时的根结点为k,最后前序遍历这个树即可。

【代码】

program tree(input,output);

var

   a       : array[0..31]of longint;

   b,r       : array[0..31,0..31]of longint;

   i,j,k,n : longint;


procedure out(i,j:longint);

begin

   if r[i,j]<>0 then begin

      write(r[i,j],' ');

      out(i,r[i,j]-1);

      out(r[i,j]+1,j);


   end;

end;


begin

   assign(input,'tree.in');

   reset(input);

   readln(n);

   for i:=1 to n do read(a[i]);

   close(input);

   for i:=1 to n do b[i,i]:=a[i];

   for i:=1 to n do r[i,i]:=i;

   for i:=1 to n do b[i,i-1]:=1;

   for j:=1 to n-1 do

      for i:=1 to n-j do begin

     b[i,i+j]:=-1;

     for k:=i to i+j do if b[i,k-1]*b[k+1,i+j]+a[k]>b[i,i+j]

        then begin

           b[i,i+j]:=b[i,k-1]*b[k+1,i+j]+a[k];

           r[i,i+j]:=k;

        end;

      end;

   assign(output,'tree.out');

   rewrite(output);

   writeln(b[1,n]);

   out(1,n);

   writeln;

   close(output);

end.

十二.最长前缀

        1.给定n个字符串,即基元

        2.给定一个字符串T,即生物体的结构

        3.要找出字符串T由基元构成的前缀,使得该前缀的长度最大

        4.N<=100,Len(T)<=500000,基元长度L <=20

      样例:

        基元:A,AB,BBC,CA,BA

        T: ABABACABAABCB

        最长前缀构成有三种方法

           A+BA+BA+CA+BA+AB

           AB+AB+A+CA+BA+AB

           AB+A+BA+CA+BA+AB

        长度为11。

      分析:

        1.为了尽快的查找到基元,我们把基元构成一个单词树,也叫trie树。如下图为样例的单词树。

        2.该树最多为26叉树,任何单词要么是某个单词的前缀,要么为从根到叶子结点组成的单词。

        3.这样我们只需要O(L)的时间即可查找到某个单词,L为单词的长度。

        

      动态规划:

         1.我们设前j个字符已经匹配,考虑前i个字符是否能匹配,主要看从i+1…j组成的字符串是否是单词

         2.因此设f(i)表示前i个字符是否已匹配,若能匹配则为真(用1表示),否则为假(用0表示)。则有:

            f[i]={1  f[j]=1且word[j+1,i]为基元

                  0 其他情况}

      时间复杂度分析

         1.每次求f(i)需要向前找f(j),i-j<L,每移动一次j需要判定word(j+1,i)是否是单词

         2.1<=i<=len(T),1<=i-j<L

         3.查找单词时间复杂度为O(L)

         4.因此总时间复杂度为O(len(T)*L2)。

         5.因为T<=500000,L<=20,因此,5*105*202=2*10^8

      样例实现过程,

        T: ABABACABAABCB,初始设f(0)=1

           1.f(1)=1,找到单词A,且f(0)=1;

           2.f(2)=1,找到单词AB,且f(0)=1;

           3.f(3)=1,找到单词BA, 且f(1)=1;

           4.f(4)=1,找到单词AB , 且f(2)=1;

           5.f(5)=1,找到单词BA , 且f(3)=1;

           6.f(6)=0,找不到以C结尾的单词;

           7.f(7)=1,找到单词CA , 且f(5)=1;

           8.f(8)=0,尽管找到单词AB,但f(6)=0;

           9.f(9)=1,找到单词BA, 且f(7)=1;

           10.f(10)=1,找到单词A, 且f(9)=1;

           11.f(11)=1,找到单词AB, 且f(9)=1;

           12.f(12)=0 ,找不到以C结尾的单词;

           13.f(13)=0,找不到以B结尾的单词;

     优化:

         1.上述过程我们可以看出,每次求i,需要向前找j,这样枚举,再加上需要查找单词,因此时间复杂

            度较高。

         2.如果要优化,显然字符串T必须至少扫描一遍,O(Len(T))不可能少。那么只能在查找单词和枚举j判

           断字符串是否是单词上做文章了。

         3.我们能否在查找单词和枚举字符串统一起来考虑呢?

           刚才解题思路是从后往前推,如果我们试着从前往后推会怎么样呢?

     样例实现过程,T: ABABACABAABCB

             1.找到单词A和AB,有f(1)=1, f(2)=1;

             2.找到单词BA和AB,有f(3)=1, f(4)=1;

             3.找到单词BA,有f(5)=1 ;

             4.找到单词CA,有f(7)=1 ;

             5.找到单词BA,有f(9)=1 ;

             6.找到单词A和AB,有f(10)=1, f(11)=1;

             7.找不到以C开头的单词,程序结束

     4.从上述过程我们可以看出,每次都是从前往后找单词,只考虑f(i)=1开始后,是否有单词,这样我们对

       字符串T并没有回溯的过程,因此时间复杂度为O(len(T)*L)。

     5.T<=50000,L<=20,因此,5*105*20=10^7

【代码】

program prefix(input,output);

const

   maxn    = 100;

   maxl    = 20;

   maxa    = 500000;

var

   ne               : array[0..maxn*maxl,'A'..'Z']of longint;

   st               : array[1..maxn*maxl]of longint;

   ok               : array[1..maxn*maxl]of boolean;

   f               : array[0..maxa]of boolean;

   i,j,k,m,h,t,l,n,tot,now : longint;

   s               : ansistring;

   c               : char;

procedure make;

var

   i : longint;

begin

   now:=0;

   for i:=1 to l do

   begin

      if ne[now,s[i]]=0 then{没有分支就加一个}

      begin

     inc(tot);

     ne[now,s[i]]:=tot;

      end;

      now:=ne[now,s[i]];

   end;

   ok[now]:=true;{标记片段结束}

end;

begin

   assign(input,'prefix.in');reset(input);

   readln(n);

   for i:=1 to n do

   begin

      readln(l);

      readln(s);

      make;{构造字母树}

   end;

   readln(s);

   l:=length(s)-1;

   close(input);

   f[0]:=true;

   for i:=0 to l do

      if f[i] then

      begin

     now:=0;

     for j:=i+1 to l do

     begin

        now:=ne[now,s[j]];{沿字母树走}

        if now=0 then{走不了就退出}

           break;

        f[j]:=f[j] or ok[now];{走出一个片段就更新}

     end;

      end;

   assign(output,'prefix.out');rewrite(output);

   for i:=l downto 1 do

      if f[i] then

     break;

   writeln(i);

   close(output);

end.

十三.求最长公共子序列

           1.给定的字符序列X=“x0,x1,…,xm-1”,序列Y=“y0,y1,…,yk-1”是X的子序列,存在X的

             一个严格递增下标序列<i0,i1,…,ik-1>,使得对所有的j=0,1,…,k-1,有xij= yj。

           2.例如,X=“ABCBDAB”,Y=“BCDB”是X的一个子序列。

           3.给出两个字串S1和S2,长度不超过5000.求这两个串的最长公共子串长度。

        分析:

          1.S1=“ABCBDAB.”

            S2=“BABCBD.”

            可以看出他们的最长公共子串有ABCB,ABCD ,BCBD等,长度为4.

            从样例分析,我们思考的方式为要找出S1串与S2串的公共子串,假设将S1固定,从第1个位置开始

            直到最后一个位置为止,与S2的各个部分不断找最长公共子串。

          当然S1也可以变化,这样我们即得出了思路:

            –枚举S1的位置i

            –枚举S2的位置j

            –找出S1的前i位与S2的前j位的最长公共子串,直到两个串的最后一个位置为止。

        动态规划:

            设f(i,j)表示S的前i位与T的前j位的最长公共子串长度。则有,

            

             f[i,j]={0  i=0|j=0

                     max{f[i-1,j],f[i,j-1] i,j>0,si<>tj}

                     f[i-1,j-1]    i,j>1 si=tj}

             时间复杂度O(n*m)

【代码】

program lcs;

const

  maxn=5001;

var

  pf,f:array[0..maxn]of longint;

  i,j,m,n:longint;

  a,b:ansistring;

function max(a,b:longint):longint;

  begin

    if a>b then

      max:=a

    else

      max:=b;

  end;

begin

  assign(input,'lcs.in');reset(input);

  readln(a);

  readln(b);

  close(input);

  n:=length(a);

  m:=length(b);

  for i:=1 to n do{f[i,j]表示a的前i个和b的前j个最长公共子序列有多长}

    begin

      for j:=1 to m do

        begin

          f[j]:=max(f[j-1],pf[j]);{从前面的状态直接转移过来}

          if (a[i]=b[j]) and (pf[j-1]+1>f[j]) then{多增加一位的情况}

              f[j]:=pf[j-1]+1;

        end;

      pf:=f;

    end;

  assign(output,'lcs.out');rewrite(output);

  writeln(f[m]-1);

  close(output);

end.

十四.卡车更新问题

            1.给出第K年卡车运输所获得的利润:R[0], R[1],…,R[k];

            2.给出第K年卡车维修费:U[0], U[1],…,U[k];

            3.给出第K年卖掉卡车,买回新车所需费用:C[0],C[1],…,C[k];

            4.设某卡车已使用过t 年,

                ①如果继续使用, 则第t+1 年回收额为R[t]-U[t],

                ②如果卖掉旧车,买进新车, 则第t+1年回收额为R[0]-U[0]-C[t] .

            5.该运输户从某年初购车日起,计划工作N (N<=20) 年, N 年后不论车的状态如何,不再工作. 为使

              这N 年的总回收额最大, 应在哪些年更新旧车? 假定在这N年内, 运输户每年只用一辆车, 而且

              以上各种费用均不改变.

          分析样例:N=4,k=5;工作4年,最多使用5年

                R: 8 7 6 5 4 2;U: 0.5 1 2 3 4 5 ;C: 0 2 3 5 8 10

                方案1:

                      第1年:新卡车,获利=8-0.5=7.5

                      第2年:旧卡车,获利=7-1=6.0

                      第3年:旧卡车,获利=6-2=4.0

                      第4年:旧卡车,获利=5-3=2.0

                       总获利:7.5+6.0+4.0+2.0=19.5

                方案二:

                      第1年:新卡车,获利=8-0.5=7.5

                      第2年:新卡车,获利=8-2-0.5=5.5

                      第3年:旧卡车,获利=7-1=6.0

                      第4年:旧卡车,获利=6-2=4.0

                       总获利:7.5+5.5+6.0+4.0=23.0

                方案三:

                      第1年:新卡车,获利=8-0.5=7.5

                      第2年:旧卡车,获利=7-1=6.0

                      第3年:新卡车,获利=8-3-0.5=4.5

                      第4年:旧卡车,获利=7-1=6.0

                      总获利:7.5+6.0+4.5+6=24.0

                方案四:

                      第1年:新卡车,获利=8-0.5=7.5

                      第2年:新卡车,获利=8-2-0.5=5.5

                      第3年:新卡车,获利=8-2-0.5=5.5

                      第4年:旧卡车,获利=7-1=6.0

                      总获利:7.5+5.5+5.5+6=24.5

          分析:

               1.从样例我们可以看出,每年我们有两种选择,要么购买新卡车,要么使用旧卡车。因此我们

                 只要对这两种情况进行考虑即可!

               2.设f[i,j]表示第i年后某卡车用了j年的卡车能获得的最大收益,则有

                  f[i,j]=max{f[i-1,j-1]+r[i]-u[i] j<>1 继续使用上一年旧卡车

                             f[i-1,L]+r[0]-u[0]-c[L] j=1 购买一辆新卡车,L表示旧车用了L年。

                             1<=i<=n,1<=j<=k,1<=l<=k  初值设f(i,j)=-∞

                  这里l和j是并列关系,时间复杂度为O(N*K)

【代码】

program truck;

const

  maxn=21;

var

  f:array[1..maxn,1..maxn]of double;

  g:array[1..maxn,1..maxn]of longint;

  r,u,c:array[0..maxn]of double;

  ans,i,j,k,l,m,n:longint;


procedure updata(key:longint;num:double);

  begin

    if num>f[i,j] then

      begin

        f[i,j]:=num;

        g[i,j]:=key;

      end;

  end;

procedure print(n,ans:longint);

  begin

    if n=1 then

      writeln(1,' ',0,' ',f[n,ans]:0:1)

    else

      begin

        print(n-1,g[n,ans]);

        write(n,' ');

        if ans=1 then{新卡车,第一年用}

          write(1)

        else

          write(0);

        writeln(' ',f[n,ans]-f[n-1,g[n,ans]]:0:1);

      end;

  end;

begin

  assign(input,'truck.in');reset(input);

  readln(n);

  readln(k);

  for i:=0 to k do

    read(r[i]);

  for i:=0 to k do

    read(u[i]);

  for i:=0 to k do

    read(c[i]);

  close(input);


  for i:=1 to n do{因为每年都必须要运,所以要考虑收益为负情况,初始化负无穷大}

    for j:=1 to k do

      f[i,j]:=1e-10;


  f[1,1]:=r[0]-u[0];{边界条件}

  for i:=2 to n do{f[i,j]表示第i年后拥有用了j年的卡车能获得的最大收益}

    for j:=1 to k do

      if j<>1 then{不是第一年用,肯定从前一年用过来的}

        updata(j-1,f[i-1,j-1]+r[j-1]-u[j-1])

      else

        for l:=1 to k do{是第一年用,今年换的}

          updata(l,f[i-1,l]+r[0]-u[0]-c[l]);

  ans:=1;

  for i:=1 to k do{找最优解}

    if f[n,i]>f[n,ans] then

      ans:=i;

  assign(output,'truck.out');rewrite(output);

  writeln(f[n,ans]:0:1);

  print(n,ans);{输出答案}

  close(output);

end.

十五.选课

        1.给定M门课程,每门课程有一个学分

        2.要从M门课程中选择N门课程,使得学分总和最大

        3.其中选择课程必须满足以下条件:

                 –每门课程最多只有一门直接先修课

                 –要选择某门课程,必须先选修它的先修课

        4.M,N<=500

     分析:

        1.每门课程最多只有1门直接先修课,如果我们把课程看成结点,也就是说每个结点最多只一个前驱结

          点。

        2.如果把前驱结点看成父结点,换句话说,每个结点只有一个父结点。显然具有这种结构的模型是树结

          构,要么是一棵树,要么是一个森林。

        3.这样,问题就转化为在一个M个结点的森林中选取N个结点,使得所选结点的权值之和最大。同时满足

          每次选取时,若选儿子结点,必选根结点的条件。

     样例分析:

        如图1,为两棵树,我们可以虚拟一个结点,将这些树连接起来,那么森林就转会为了1棵树,选取结点

        时,从每个儿子出发进行选取。显然选M=4时,选3,2,7,6几门课程最优。              

     动态规划:

         1.如果我们单纯从树的角度考虑动态规划,设以i为根结点的树选j门课程所得到的最大学分为f(i,j),

           设虚拟的树根编号为0,学分设为0,那么,ans=f(0,n+1)

         2.如果树根选择1门功课,剩下j-1门功课变成了给他所有儿子如何分配的资源的问题,这显然是背包

           问题

         3.设前k个儿子选修了x门课程的最优值为g(k,x),则有

           f[k,x]=max{f[k-1,x]  第k个儿子不选

                      f[k-1,x-y]+f[son[k],i-1]+a[k] 第k个儿子选修y门}

           其中:0<=x<=j-1,ans=g(son(0),n+1)

     进一步分析:

         1.上述状态方程,需要枚举每个结点的x个儿子,而且对每个儿子的选课选择,需要再进行递归处理。

         2.当然这样可以解决问题,那么我们还有没有其他方法呢?

      转化为二叉树

         如果该问题仅仅只是一棵二叉树,我们对儿子的分配就仅仅只需考虑左右孩子即可,问题就变得很简

         单了。因此我们试着将该问题转化为二叉树求解。

          

         图2就是对图1采用孩子兄弟表示法所得到的二叉树

      动态规划:

          1.仔细理解左右孩子的意义(如右图):  

               左孩子:原根结点的孩子           

               右孩子:原根结点的兄弟  

          2.也就是说,左孩子分配选课资源时,根结点必须要选修,而与右孩子无关。                   

          3.因此,我们设f(i,j)表示以i为根结点的二叉树分配j门课程的所获得的最大学分,则有,

             f[i,j]=max{f[i[r],j] 不选根结点

                        f[i[l],k]+f[i[r],j-k-1]+a[i] 选修根结点}

                        0<=k<j<n, i ∈(1..m)

          4.时间复杂度O(mn^2)

【代码】

{$inline on}

program course;

const

  maxn=1001;

var

  ne,f:array[0..maxn,0..maxn]of longint;

  g,fa:array[0..maxn*2,0..maxn]of longint;

  t,ts,v,pr:array[-1..maxn]of longint;

  ok:array[0..maxn]of boolean;

  i,j,k,m,n:longint;

procedure work(now:longint);inline;

  var

    i,j,k,bas:longint;

  begin

    for i:=1 to t[now] do

      work(ne[now,i]);

    bas:=ts[now-1]+1;

    for i:=bas+1 to bas+t[now] do{f[i,j]表示i子树内选j的最大价值}

      for j:=1 to m do{g[i,j]是给每个节点分配的内部背包的空间}

        begin

          g[i,j]:=g[i-1,j];

          for k:=1 to j do

            if g[i-1,j-k]+f[ne[now,i-bas],k]>g[i,j] then

              begin

                g[i,j]:=g[i-1,j-k]+f[ne[now,i-bas],k];

                fa[i,j]:=k;{记录决策点}

              end;

        end;

    for i:=m downto 1 do{计算f[i,j]}

      f[now,i]:=g[t[now]+bas,i-1]+v[now];

  end;

procedure find_ans(now,num:longint);inline;

  var

    i,j,bas:longint;

  begin

    if num=0 then

      exit;

    ok[now]:=true;

    dec(num);

    bas:=ts[now-1]+1;

    for i:=t[now] downto 1 do{根据决策点递归求答案}

      begin

        find_ans(ne[now,i],fa[bas+i,num]);

        dec(num,fa[bas+i,num]);

      end;

  end;

begin

  assign(input,'course.in');reset(input);

  readln(n,m);

  inc(m);

  for i:=1 to n do

    begin

      readln(pr[i],v[i]);{临接表}

      inc(t[pr[i]]);

      ne[pr[i],t[pr[i]]]:=i;

    end;

  close(input);

  for i:=0 to n do{给每个节点分配空间}

    ts[i]:=ts[i-1]+t[i]+1;

  work(0);

  assign(output,'course.out');rewrite(output);

  writeln(f[0,m]);

  find_ans(0,m);

  for i:=1 to n do

    if ok[i] then

      writeln(i);

  close(output);

end.


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值