浅谈DP

来源:互联网 发布:一个人能备案几个域名 编辑:程序博客网 时间:2024/06/18 03:10

DP

dp——可以说是竞赛里地位不可被取代的一种极其常见的解题方法,也是一种思维方式.

这种思维方式,以及技巧,是极其灵活的,也是非常有趣的.
立下此Blog,总结巩固一些我所掌握的DP知识.

前言:

掌握dp的能力,是要在无数道题目的基础上,再加以灵活运用,灵活变通所巩固而来的,所以接下来让我们在一道道题目中去看看DP的用处及一些技巧.

难度:

:入门类dp题.

:在入门dp上嵌套一些知识点,题面看起来复杂一点的题目.

:一些形式比较复杂,比较难设状态或比较难以转移的dp题目.

:很有挑战性的dp题,以至于做时不知这是一道dp题,非常新颖或巧妙.

:即使有了状态和听了千万遍才听懂的转移,但还是打不出或难以打出的dp题目.


T1:

Problem:

给定一个长度为n的序列A,求此序列的最长不下降子序列.

Data Constraint:

对于50%的数据,n<=5000,Ai<=105
对于100%的数据,n<=500000Ai<=109

Solution:

50%

对于n<=5000,可以用O(n2)的做法,即设fi表示序列前i个数的最长不下降自序列.

转移:fi=max(f[j]+1)|(Ai>=Aj)

100%

那么对于n<=500000.

考虑另设状态.

我们设fi表示最长不下降子序列长度为i时的这个序列的第i个数(即最尾一个数)的最小值.

那么转移就显然了:

对于每一个Ai,判断能更新到fi的最大i值,因为fi必定是严格不减的,所以可以考虑二分.

故总的时间复杂度时大致为O(nlog2n),实际不及这个数.

Code

var          a,f:array[1..100000] of longint;          n,i,k,len:longint;  function find(len,n:longint):Longint;  var          l,r,mid:longint;  begin          l:=1; r:=len;          while l<=r do         //注意这里的=号,不可省略,举个例子你就知道为什么了        begin                  mid:=(l+r) div 2;                  if f[mid]>n then r:=mid-1 else l:=mid+1;     //注意这里的>号,我们找的是最长不下降,故当f[mid]<=n时,l的值都应该增加.        end;          exit(l);             //f[l]即ai能更新的最远位置,此二分能保证f[l-1]<=n,且f[l-1]<>f[l],故f[l]一定可被更新.end;  begin          readln(n);          for i:=1 to n do                  read(a[i]);          len:=1;          f[1]:=a[1];          for i:=2 to n do          begin                  k:=find(len,a[i]);                  f[k]:=a[i];                  if k>len then len:=k;     //即更新f[l]和最长不下降串的长度len        end;          writeln(len);  end.  

difficulty:*


T2:

https://jzoj.net/senior/#contest/show/1923/1

Problem:

给定一个长度为n的序列P,我们想把这个序列变成单调不增或单调不减,若把一个数改大,需要b1的费用,改小需要b2的费用,问,如何花费最小的费用使得序列满足条件.

Data constraint:

数据保证不同的Pi不超过T=1000个.

Solution:

一个显然的性质——对于此题,我们只需考虑单调不减,因为把序列反过来后就成了单调不增.

75%

那么,对于m1=m2=1,问题显然就成了最长不下降子序列.
而最长不下降子序列是可以在O(nlog2n)内求出的.
期望得分:60~75

100%

最长不下降子序列其实是一种类似贪心的dp,当数改动的消费不一样(即m1m2)时,贪心的dp显然不满足无后效性.

那怎么让它变得有后效性呢?

其实我们仔细想想,可以发现,每一个数都是针对其前一个数对应而言的, 也就是对于每一个数只要不要小于其前一个数即可.

一种极其简单的设法:我们可以设状态fi,j表示将前i个人单调不减后,第i个人的权值改为j的最小花费.

可是Pi<=106n<=5104,空间会爆.

怎么办?

想到不同的Pi不超过1000个, 所以我们可以把Pi离散化.

那么转移显然:fi,j=Max(f[i1][k])+sum(p[i],j),而f[i1][k]可以预处理,故转移O(1),总的时空复杂度为:

时间复杂度:O(nT)
空间复杂度:O(nT)

difficulty:**


T3

https://jzoj.net/senior/#main/show/3735

Problem:

[x..y]中的有趣数.
有趣数的定义如下:若一个数字至少有一半以上的数位上拥有相同的数字,则这个数为有趣数.

Data constraint:

对于20%的数据,1<=x<=y<=100000
对于30%的数据,1<=x<=y<=10000000
对于40%的数据,1<=x<=y<=3108
对于100%的数据,1<=x<=y<=1018

Solution:

20%:

直接模拟即可

30%:

我们转化数字的时候不用字符串,用整数代替可以加快速度,简单说就是字符串太慢.

40%

显然,此题满足区间减法,即answer=fans(y)fans(x1).
可以想到数位DP,其实就是100%的做法.

100%

很明显,这是一道数位DP的题目,但是,我做的数位DP题还不多,所以一道入门级别的题目需要讲的比较啰嗦一些,其实只是方便自己巩固。

Wrong method

当得知是数位DP后,我们很容易可以想到这么设状态:
f[i][j][k][p]表示构造的前i位数,第i位选jk这个数字出现的次数与其他数字出现次数的差为p时的方案数.

最后结果就是

t=1ni=1aij=19k=0tf[t,i,j,k]

这样子设状态,仿佛把问题简化了许多,但我们看看到底为什么不能这么设状态.

这么设状态,我们需要枚举第i位选的数和判断的数,i+1位同样,但你会发现转移无从下手(因为第i位选的数与i+1位如果直接相加,肯定会有重复)

错误原因:状态设错,导致无从转移.

解法一

设此状态时我们可以采取数位DP中的经典设法:

假设我求fans(x)的值,状态f[i][same][zero]i表示构造的数到了第i位,same=0表示前i位构造的数等于x的前i位,same=1表示前i位构造的数小于x的前i位,zero=0表示当前构造的数没有前导0zero=1表示当前构造的数有前导0.

设完状态之后,我们总感觉少了点什么,哦,这道题要我们求相同数字的个数要至少占整个数长度的一半,那我就把状态设成f[i][same][zero][key][p],新增key,p分别表示key这个关键字出现了p次.

可通过上面的Wrong method可以得知此状态在转移时是会有重复的.

那问题来了,怎么避免这种重复?

我们想,对于每一个有趣数,至多有两个占一半数位的数字,言下之意就是我们只需枚举这个占一半数位的数字,然后再减去特殊情况有两个数字的即可.

那么我们设p为关键字,key为其出现的次数.

这样设了状态之后,转移就变得简单了,不过还有很多的细节之处需要注意,如为什么要有前导0,它的作用是什么,怎么处理其对应的情况,我们在code里详细解释.

Code-1

var        ss:string;        s:array[0..20] of int64;        f:array[0..20,0..1,0..1,-40..40] of int64;        t:array[0..9] of int64;        i,j,k,p,same1,zero1,key1,same2,next:longint;        zero2,key2,ans,tot,x,y:int64;function fans(x1,x2:int64):int64;begin        fillchar(f,sizeof(f),0);        f[0,0,1,0]:=1;                   //此状态很重要,及通过下面一样的以前导0的思想,导出所有的状态.        fans:=0;        for i:=0 to length(ss)-1 do                for same1:=0 to 1 do                        for zero1:=0 to 1 do                                for key1:=-20 to 20 do                                        for next:=0 to 9 do                                        begin                                                if (x1+x2>0) and ((next>0) or (zero1=0)) and (next<>x1) and (next<>x2) then continue;           //这里是判断特殊情况,即有两个关键字时,当枚举的不是前导0时,next必须为x1或x2.                                                if (same1=0) and (next>s[i+1]) then break;        //当构造的数已经大于原数则可以退出循环.                                                if (same1=1) or (next<s[i+1]) then same2:=1 else same2:=0;      //显然.                                                if (zero1=1) and (next=0) then zero2:=1 else zero2:=0;           //这里是重点,即前导0的用处,只有当(zero1=1)and(next=0)时,这个数就是0,则这个数可以作为一个前导,来“导出”下面一些不足n位的数,使得可以一次算尽所有1~x里的所有数.                                                key2:=key1;                                                if (zero2=0) then                                                begin                                                        if (next=k) then key2:=key1+1 else key2:=key1-1;                                                end;   //只有当不为前导时,才有关键字可言.                                                inc(f[i+1,same2,zero2,key2],f[i,same1,zero1,key1]);  //转移.                                        end;        for same1:=0 to 1 do                for key1:=0 to 20 do                begin                        if (x1+x2>0) and (key1>0) then break;   //即处理有两个关键字时,key1的值只能为0.                        inc(fans,f[length(ss),same1,0,key1]);   //累加答案.                end;end;function answer(x:int64):int64;begin        str(x,ss);        for i:=1 to length(ss) do                s[i]:=ord(ss[i])-48;    //转化        answer:=0;        for k:=0 to 9 do                inc(answer,fans(0,0));  //处理关键字k        for k:=0 to 9 do                for p:=k+1 to 9 do                        dec(answer,fans(k,p)); //处理含有两个关键字的有趣数end;begin        assign(input,'odometer.in'); reset(input);        assign(output,'odometer.out'); rewrite(output);        readln(x,y);        writeln(answer(y)-answer(x-1));  //即满足区间减法        close(input); close(output);end.

difficulty:***

T4

https://jzoj.net/senior/#contest/show/1947/1

Problem:

给定正整数n,对应序列a[1..n],c[1..n],每一个时刻可以选择一个数,也可以不选,选中第i个数所需的花费为Ci,其中ai表示第t个时刻选了第i个数之后,第t+ai个时刻之前必须再选一次i,如若不能满足此条件便停止选择, 输出对应时刻,并求出最小花费;当时刻到达m1时,即可停止选择,输出+oo,并求出最小花费,一开始拥有q费用.

Data Constraint:

对于50%的数据,1<=n<=3
对于80%的数据,1<=Ai<=4
对于100%的数据,1<=n<=4,1<=q,ci<=maxlongint,1<=ai<=9

Solution:

50%-Wrong method:

第一次做时,我是想到了类似周期的方法,但很明显这种方法是错的,例如数据如下:
n=2  m=30  q=20000
Ai=3    5
Ci=100 215
按照周期思想,A1应该只被算(m1)div ai次,即只算9次,但在实际情况中,我们发现,35当有公倍数时,3总得换个时刻选,所以最后应该要选10次.

100%:

因为1<=n<=4,所以很容易想到设状态f[time][i][j][k][p]表示到第time个时刻,其中第一个数距被选还差i个时刻,第二个数距被选还差j个时刻,第三个数距被选还差k时刻,第四个数距被选还差p个时刻的最少花费.

其中当i,j,k,p=1时表示的是能选择的最后期限,即过了这一时刻就停止选择了.

显然有方程:
f[time][i][j][k][p]=>f[time+1][i1][j1][k1][p1]
f[time][i][j][k][p]=>f[time+1][a1][j1][k1][p1]
f[time][i][j][k][p]=>f[time+1][i1][a2][k1][p1]
f[time][i][j][k][p]=>f[time+1][i1][j1][a3][p1]
f[time][i][j][k][p]=>f[time+1][i1][j1][k1][a4].

最后直接判断f[m1]即可.

现在麻烦的是,需要分类讨论来解答n=1,2,3,4时的情况,使代码量一下增大.

但其实有一种及其简单又有效的方法,即把n<4时的数全补上,把A[n+1..4]=9即可解决.

Code:

var        a,c:array[1..4] of longint;        f:array[0..1000,-1..9,-1..9,-1..9,-1..9] of longint;        n,m,q,i,j,k,p,sum,t,time:longint;function max(x,y:longint):longint;begin        if x>y then exit(x) else exit(y);end;begin        readln(n,m,q);        for i:=1 to n do                read(a[i]);        for i:=n+1 to 4 do                a[i]:=9;        for i:=1 to n do                read(c[i]);        fillchar(f,sizeof(f),255);        f[0,a[1],a[2],a[3],a[4]]:=q;        for time:=0 to m-2 do                for i:=1 to a[1] do                        for j:=1 to a[2] do                                for k:=1 to a[3] do                                        for p:=1 to a[4] do                                        begin                                                f[time+1,i-1,j-1,k-1,p-1]:=max(f[time+1,i-1,j-1,k-1,p-1],f[time,i,j,k,p]);                                                f[time+1,a[1],j-1,k-1,p-1]:=max(f[time+1,a[1],j-1,k-1,p-1],f[time,i,j,k,p]-c[1]);                                                f[time+1,i-1,a[2],k-1,p-1]:=max(f[time+1,i-1,a[2],k-1,p-1],f[time,i,j,k,p]-c[2]);                                                f[time+1,i-1,j-1,a[3],p-1]:=max(f[time+1,i-1,j-1,a[3],p-1],f[time,i,j,k,p]-c[3]);                                                f[time+1,i-1,j-1,k-1,a[4]]:=max(f[time+1,i-1,j-1,k-1,a[4]],f[time,i,j,k,p]-c[4]);                                                if f[time,i,j,k,p]>=0 then t:=time;                                        end;        sum:=-2;        for i:=1 to a[1] do for j:=1 to a[2] do for k:=1 to a[3] do for p:=1 to a[4] do                if f[m-1,i,j,k,p]>sum then sum:=f[m-1,i,j,k,p];        if sum>-1 then        begin                writeln('+oo');                writeln(sum);        end        else        begin                writeln(t);                for i:=1 to a[1] do for j:=1 to a[2] do for k:=1 to a[3] do for p:=1 to a[4] do                        if (f[t,i,j,k,p]>sum) then sum:=f[t,i,j,k,p];                writeln(sum);        end;end.

difficulty:**


T5

https://jzoj.net/senior/#main/show/1340

Problem

给定n个长度为ai,宽度为1的矩形,求出摆列矩形的方式使得其形成的新不规则图形的周长最长,并求出保证最长周长的方案数.

Data constraint

对于40%的数据,有N<=6;
对于100%的数据,有N<=15
对于100%的数据,有0<a[i]<=100,所以可以设状态f[i][j]表示以i为结尾,选了的数的状态为j的最长周长,转移显然,位运算即可.

difficulty:*

T6

https://jzoj.net/senior/#main/show/1397

Problem

给定一个环,指针一开始指向第一个数,每次可以取指针周围相邻的k个数,选了的数加上其对应的权值,每次转动指针w次,所需的费用是wmaxmax为当前剩下数字的最大值.
求当所有数都被选完后所需的最小费用.

Data constraint

对于20%的数据,n≤10,k≤3;
对于40%的数据,n≤100,k≤10;
对于60%的数据,n≤500,k≤20;
对于100%的数据,n≤2000,k≤500;

Solution

这道题比赛时就知道是DP了,但是这题的DP很巧,思维很缜密,值得好好总结.

思维策略:
因为选数的代价是一定的,故可以把问题转化为如何转动指针使得代价最小.

而我们知道,每次取数时,把能取的数都取了一定是最优的.

所以我们先把1knnk+1全取了,那么问题就转化为:在k+2nk当中左右两边删数的问题.

状态
f[i][j][0]表示还剩下ij这个区间的数没选时,指针停留在ik1的最小代价.
f[i][j][1]表示还剩下ij这个区间的数没选时,指针停留在j+k+1的最小代价.

状态设的很巧!

转移
f[i,j,0]=min{f[i1,j,0]+max[i1,j],f[i1,j,1]+max[i1,j](nlen),f[i,j+1,1]+max[i,j+1](nlen+1)}
f[i,j,1]=min{f[i,j+1,1]+max[i,j+1],f[i,j+1,0]+max[i,j+1](nlen),f[i1,j,0]+max[i,j1](nlen+1)}

这里的n是已经减了(2k+1)的,len是区间[i,j]的长度.

理解
只讨论第一个状态转移的思维方式,另一状态是一样的.

f[i1,j,0]+max[i1,j]这个状态应该很容易理解.

f[i1,j,0]这个状态时,指针指向ik2,现在指针要转到ik1,因为状态设的是还剩下[i,j]没选,故转动代价是max[i1,j].

f[i1,j,1]+max[i1,j](ni)这个状态就有一点抽象了.

f[i1,j,1]这个状态时,首先看看指针现在指向哪,指向j+k+1,那么现在它要转移到ik1去,就得思考它到底需要转多少次?
很明显不用走的路程是(j+k)(ik1),故总共有(j+k)(ik1)+1=ji+2+2k=len+2k+1个格不用走,剩下的n(len+2k+1)便是要走的.

f[i,j+1,1]+max[i,j+1](ni+1)这个状态与②的思想是类似的,但有一点不同,就是指针转动的路程要多转一个.为什么?
状态f[i,j+1,1]f[i,j,0]相比,第一个状态中的第j+1个数是得选掉的,所以我们需要往右移一个指针,再往回时,便多走了两步,但相对于第二个状态来说,指针所处的位置是ik2,现在是在ik1,故可以少走一步,故总共比上个状态要多走一步.

f[i][j][1]的转移思路是一样的,关键在于对于状态和整体思想的理解.

整体思想:
所以让我们分析一下这题的整体思想——把状态设好后,把转移分成两步,一、往当前指针方向走一步,二、跳到链的另一端并继续走一步.
其中跳到链的另一端时又分两种情况讨论,讨论所需要走的步数!

总结:
此题重在思维,实现起来很简单,但还需要注意一些细节,如区间最大值的预处理,一些不可能转移状态的预处理,设为无穷大,最后f[i][i][0/1]时会发现其实还要再走一步才能选掉i,所以还要加上ai.

DP的思路是很广的,关键在于如何挖掘题目的已有条件,挖掘隐含的性质,把问题简单化,设下状态转移.

这题的状态设的很巧,以至于把这题完完整整的想了三天才明白,是一道好题!

difficulty:***

T7

https://jzoj.net/senior/#main/show/1667

Problem

在n*m的棋盘里能放多少个炮.

Data constraint

除了在3个格子中都放满炮的的情况外,其它的都可以.
100%的数据中N,M不超过100
50%的数据中,N,M至少有一个数不超过8
30%的数据中,N,M均不超过6

Solution

一开始看题就联想到了很多有关于类似放马,放国王的问题.

放马的是可以通过找规律得出答案,放国王的可以搜索.

但当看到数据之后,瞬间醒悟。

n,m<=100,一眼就是DP题.

容易发现,其实题目可以转化为在一个nm的棋盘里放棋子,并保证一行一列至多有两个棋子.

那就按照想法设状态-设f[i][j][k]表示前i行中,目前有j列放了一个棋子,k列放了两个棋子的方案数.

注意,不能用f[i]去推f[i+1],否则会算重.

dp的时候注意一些细节就好了,总共分六类情况讨论,注意没有“一行同竖着放两个棋”的奇葩转移.

diffcultly:**

T8

https://jzoj.net/senior/#main/show/1330

Problem

给定一个仅包含j,z两个字符的字符串,可以交换两个字符m次,求最终串里能有多少个jz.

Data constraint

【数据规模】
对于10%的数据,有N≤10;
对于30%的数据,有K≤10;
对于40%的数据,有N≤50;
对于100%的数据,有N≤500,K≤100。

Solution

这道题的思路很巧妙.是一道好题.

首先,我们先看看交换两个字符这一操作是否可以更改.

因为我发现如果就以交换两个字符去做,很难设状态,并且很难转移.

并且这还不符合dp的无后效性.

但其实我们可以把它转化成无后效性.

以最一般的思路设状态,设fi,j,k表示前i个字符当中,把jj字符变成z字符,把kz字符变成j字符.

先不考虑转移,当这个状态中的j=k时,其实很显然就是一个置换了,故dp的无后效性就解决了.

于是转移我们就可以分四种情况讨论,按照sisi1组成的jj,jz,zj,zz这四种关系去推.

这里转移有个技巧,就是由fi2去推fi,而fi2的状态并不影响以后的Si1,Si

除了状态设的巧妙,转移巧妙,这题还让我领悟到dp的一些技巧,如初始值的设立和一些冗余状态带来的错误.

一定要注意这些细节,比如说当j,k小于0时,是不能作为状态的,当j=k=0时,也是需要特判.

并且这题还有最重要的一个细节,由于状态的巧妙,当一个串中一个字符的个数都已经小于答案了,那么就不能置换,所以答案一定不能大于j,z两种字符出现次数之中的任意一个.

Code

uses math;var        l:array[0..500] of longint;        f:array[0..500,-1..100,-1..100] of longint;        s:ansistring;        n,k,i,j,t,ans,tot1,tot2,kk:longint;begin        readln(n,t);        readln(s);        for i:=1 to length(s)-1 do        begin                if (s[i]='j') and (s[i+1]='z') then inc(ans);                l[i+1]:=ans;                if s[i]='j' then inc(tot1) else inc(tot2);        end;        if s[i+1]='j' then inc(tot1) else inc(tot2);        kk:=min(tot1,tot2);        for j:=0 to t do                for k:=0 to t do                        f[0,j,k]:=-maxlongint;        f[0,0,0]:=0;        for i:=2 to n do                for j:=0 to min(i,t) do                        for k:=0 to min(i,t) do                        begin                                if j+k=0 then                                begin                                        f[i,j,k]:=l[i];                                        continue;                                end;                                if (s[i-1]='j') then                                begin                                        if s[i]='z' then f[i,j,k]:=f[i-2,j,k]+1 else                                        begin                                                if j>0 then f[i,j,k]:=f[i-2,j-1,k]+1;                                        end;                                end                                else                                begin                                        if s[i]='z' then                                        begin                                                if (k>0) then f[i,j,k]:=f[i-2,j,k-1]+1;                                        end                                        else                                        begin                                                if (j>0) and (k>0) then f[i,j,k]:=f[i-2,j-1,k-1]+1;                                        end;                                end;                                f[i,j,k]:=max(f[i,j,k],f[i-1,j,k]);                        end;        for i:=0 to t do                if (f[n,i,i]>ans) then ans:=f[n,i,i];        writeln(min(ans,kk));end.

diffcultly:***

T9

https://jzoj.net/senior/#main/show/1331

Problem

给定n个i*100米的高度,再给定一个初始能量,每次最多跳不超过能量的米数,跳到第i个高度会有一定的收益,保证n个高度上的收益都能拿到,求最后最大的收益.

Data constraint

对于10%的数据,有N≤10;
对于20%的数据,有N≤100;
对于40%的数据,有N≤1000;
对于70%的数据,有N≤100000;
对于100%的数据,有N≤2000000。
保证对于所有数据,教主都能吃到所有的能量球,并且能量球包含的能量之和不超过2^31-1。

Solution

很容易想到状态:
f[i][j]表示跳了i次,最后一次跳到高度j的最大收益.

很明显:f[i,j]=max{f[i1,k]sum[k]+sum[j]j100}|f[i1,k]>=j100

复杂度O(n³),能通过20%的数据.

很容易又发现,每次跳一定会比上次跳的高,所以跳了几次这一维可以省略.

于是复杂度变成O(n²)了,可以通过40%的数据.

再很容易发现,方程可以用线段树维护,但由于线段树常数太大,可以拿70~80分.

再观察一遍dp式子:f[i]=maxf[j]sum[j]+sum[i]i100

k<j<i,当f[j]+sum(j+1,i)>f[k]+sum(k+1,i)
f[j]f[k]>sum[j]sum[k]时,k这个决策点是没用的,于是有用的决策可以用单调队列维护.

const        maxn=2000000;var        sum,f,a,d,s:array[0..maxn] of longint;        n,m,i,h,t:longint;begin        readln(n,m);        for i:=1 to n do        begin                read(a[i]);                sum[i]:=sum[i-1]+a[i];        end;        d[0]:=m;        s[0]:=m;        h:=0;        t:=1;        for i:=1 to n do        begin                while (s[h]<i*100) and (h<t) do inc(h);                f[i]:=sum[i]-i*100+d[h];                while (f[i]-sum[i]>d[t]) and (t>0) do dec(t);                inc(t);                d[t]:=f[i]-sum[i];                s[t]:=f[i];        end;        writeln(f[n]);end.

但其实单调队列并没有什么实际用处,这道题之所以能优化成O(n),是因为贪心的思想.

每次只要跳到比上次高且最矮的地方就好了.

const        maxn=2000000;var        sum:array[0..maxn] of longint;        f:array[0..maxn] of longint;        A:array[1..maxn] of longint;        n,m,i,j,k,w,ans:longint;begin        readln(n,m);        for i:=1 to n do        begin                read(a[i]);                sum[i]:=sum[i-1]+a[i];        end;        f[0]:=m;        w:=0;        for j:=1 to n do                for k:=w to j-1 do                        if (f[k]>=j*100)  then                        begin                                f[j]:=sum[j]-j*100+f[k]-sum[k];                                w:=k;                                break;                        end;        writeln(f[n]);end.

线段树版本:

const        maxn=2000000;var        tree:array[0..2*maxn+1000000] of longint;        A,sum,f:array[0..maxn] of longint;        n,m,i,j,s,t:longint;function max(x,y:longint):longint;begin        if x>y then exit(x) else exit(y);end;procedure modify(x,l,r,s:longint);var        mid:LONGINT;begin        if l=r then tree[x]:=max(tree[x],s) else        begin                mid:=(l+r) shr 1;                if mid>=t then modify(x shl 1,l,mid,s) else modify(x shl 1+1,mid+1,r,s);                tree[x]:=max(tree[x shl 1],tree[x shl 1+1]);        end;end;procedure find(x,st,en,l,r:longint);var        mid:longint;begin        if (st=l) and (en=r) then s:=max(s,tree[x]) else        begin                mid:=(st+en) shr 1;                if mid>=r then find(x shl 1,st,mid,l,r) else                if mid<l then find(x shl 1+1,mid+1,en,l,r) else                begin                        find(x shl 1,st,mid,l,mid);                        find(x shl 1+1,mid+1,en,mid+1,r);                end;        end;end;begin        readln(n,m);        for i:=1 to n do        begin                read(a[i]);                sum[i]:=sum[i-1]+a[i];        end;        fillchar(tree,sizeof(tree),$80);        f[1]:=a[1]+m-100;        t:=f[1] div 100;        modify(1,1,n,f[1]-a[1]);        for j:=2 to n do        begin                s:=-maxlongint;                find(1,1,n,j,n);                f[j]:=sum[j]-j*100+s;                if j*100<=m then f[j]:=max(f[j],m-j*100+sum[j]);                t:=f[j] div 100;                modify(1,1,n,f[j]-sum[j]);        end;        writeln(f[n]);end.

diffcultly:**


T10

https://jzoj.net/senior/#contest/show/2013/2

Problem

给定n个人的“上司“,求出一个排列,使得每个人的上司在其前面,且一个人的下司一样按照等级顺序排好.

Data constraint

对于20%的数据,有N ≤ 9;
对于40%的数据,有对于所有K,有K ≤ 2;
对于60%的数据,有N ≤ 100;
对于100%的数据,有T ≤ 10,N ≤ 1000,K ≤ N。

Solution

考试时没有发现其实这是一棵树.

这点首先得想到,以后遇到这类有关系的题目,且只有一个父亲的,一定要想到树.

然后再看一下数据范围和取模,hh,考虑一下dp吧.

这是一道质量比较高的树形Dp,也是我不擅长的题目类型.

我们先来看一下要怎么求方案数.

假设,当前一个父亲x,它有n个儿子,分别为x1,x2xn.

因为这n个儿子必须是有序的,当这n个儿子都没有儿子时,那么总的方案数就是1.

而当有一个儿子有儿子时,我们就可以拿x的儿子去与其构成不同的排列,方案数就会增加.

那么怎么算才不会把方案数算重?

f[i]表示以i为根的子树的方案数.

我们可以这么想,如果一个父亲x,它的儿子x1xn全部的答案都已经算好了,如何更新g[x]

假设现在算xi对于x的贡献,那么很显然,g[x]=g[x]g[xi],再进一步,思考可以得出,只有xi+1xn的点构成的子树可以穿插到gi的子树中,那么怎么算这个“穿插部分”的答案?

很显然,i个数插进j个空里可以用组合去求,实际上组合数是一个f[i,j]=f[i,j1]+f[i1,j]

虽然这个组合数很好码,但是要发现并不简单,其实可以用普通的排列组合先试一下,随后再推一推这个式子

那么这道题就可以很完美解决了.

由于是第一次做比较难的树形dp,花的时间比较多,也要多补补树形dPde知识,so interesting!




https://jzoj.net/senior/#main/show/3889 (位运算DP)
https://jzoj.net/senior/#main/show/1221(状压DP,学习lin大爷做法)
https://jzoj.net/senior/#main/show/3844(树形DP)
https://jzoj.net/senior/#main/show/3876(普通优化DP)
https://jzoj.net/senior/#main/show/3805(树形DP)
https://jzoj.net/senior/#main/show/3803(状压DP)
https://jzoj.net/senior/#main/show/4886(KMP嵌套DP)
https://jzoj.net/senior/#main/show/1771(单调和堆优化)
https://jzoj.net/senior/#main/show/3784(DP,不同方法)
https://jzoj.net/senior/#main/show/3792(DP,不同方法)
https://jzoj.net/senior/#main/show/1292(DP,不同方法)
https://jzoj.net/senior/#main/show/1293(DP,不同方法)
https://jzoj.net/senior/#main/show/1281(DP,不同方法)
https://jzoj.net/senior/#main/show/3046(DP,不同方法)
https://jzoj.net/senior/#main/show/3019(DP,不同方法)
https://jzoj.net/senior/#main/show/3034(树形DP)
https://jzoj.net/senior/#main/show/3056(DP,不同方法)
https://jzoj.net/senior/#main/show/3067(状压DP)
https://jzoj.net/senior/#main/show/1857(DP,不同方法)
https://jzoj.net/senior/#main/show/2545(DP,不同方法)
https://jzoj.net/senior/#main/show/2540(位运算优化DP)
https://jzoj.net/senior/#main/show/3095(区间DP)
https://jzoj.net/senior/#main/show/1265(数位DP)
https://jzoj.net/senior/#main/show/1210(DP,不同方法)
https://jzoj.net/junior/#main/show/1521(数位DP)
https://jzoj.net/junior/#contest/show/1373/3(DP,不同方法)
https://jzoj.net/senior/#contest/show/1929/2(DP,不同方法)
https://jzoj.net/senior/#contest/show/1916/3(新颖的区间DP)

单调优化,堆优化,这两种优化必须熟练掌握.
四边形不等式优化,斜率优化,需要了解透彻.

1 0
原创粉丝点击