【缄*默】 #DP# 各种DP的实现方法(更新ing)

DP =「状态」+「阶段」+「决策」

基本原理 = 「有向无环图」+「最优子结构」+「无后效性」

 

目录

一. 线性DP

{ 1.概念引入 }

{ 2.例题详解 }

【例题1】caioj 1064 最长上升子序列

【例题2】caioj 1068 最长公共子序列

【例题3】洛谷 p1216 数字三角形

【例题4】poj 2279 Picture Permutations

【例题5】最长公共上升子序列(LCIS)

【例题6】三个字符串的最长公共子序列

【例题7】poj 3666 Making the Grade

【例题8】noip 2008 传纸条

二. 背包DP

(1)0/1背包

【思路1】初步分析

【思路2】滚动数组优化空间

【思路3】转化为一维

【例题】poj 1015  Jury Compromise

【 0/1 背包练习题】

(2)完全背包

【具体实现】转化为一维

【例题】TYVJ 1172

【完全背包练习题】

(3)多重背包

【思路1】直接拆分法

【思路2】二进制拆分法

【例题】poj 1742 Coins

【多重背包练习题】

(4)分组背包

【分组背包练习题】

三. 区间DP

{ 1. 概念引入 }

{ 2. 例题详解 }

【例题1】洛谷 p1430 序列取数

【例题2】洛谷 p4170 涂色

【例题3】洛谷 p4342 Polygon

【例题4】洛谷 p1880 石子合并

【例题5】洛谷 p1063 能量项链

【例题6】凸多边形的划分

【例题7】括号配对

【例题8】括号配对(升级版)

【例题9】loj 10151 分离与合并

【例题10】洛谷 p1005 矩阵取数

四. 树形DP

{ 1.经典问题 }

{ 2.相关知识点总结 }

{ 3.例题总结 }

【例题1】洛谷 p1352 没有上司的舞会

【例题2】洛谷 p2014 选课(背包类树形DP)

五. 图论DP

六. 数位DP+状压DP

七. 倍增优化DP

八. 数据结构优化DP

九. 单调队列优化DP

十. 斜率优化

十一. 四边形不等式


 

零. 相关概念辨析

(1)递归和递推

1. 递归从最终目标出发,将复杂问题化为简单问题,逆向解答。

2. 递推从简单问题出发,一步步的向前发展,正向解答。

(2)记忆化搜索

先将数组初始化,再在递归的过程中判断此处是否已经计算过。

(3)刷表法和填表法

填表法:利用方程和上一状态推导现在的状态(已知条件,填答案)。

刷表法:利用当前的状态,把有关联的下一状态都推出来。

 

一. 线性DP

{ 1.概念引入 }

解决一些线性区间上的最优化问题,会用到DP的思想,这种问题可以叫做线性DP。

DP的阶段沿各个维度线性增长,从边界点开始,有方向地向状态空间转移、扩展。

若“决策集合中的元素只增多不减少”,可以维护一个变量记录决策集合信息,避免重复扫描。

需要考虑:一个后续阶段的未知状态,需要哪些前阶段的已知状态去更新。

经典模型:最长上升子序列(LIS)、最长公共子序列(LCS)、最大子序列和、数字三角形等。

那么首先我们从这几个经典的问题出发开始对线性DP的探索。

 

{ 2.例题详解 }

【例题1】caioj 1064 最长上升子序列

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1064】最长上升子序列
n个不相同的整数组成的数列a(1)、a(2)、……、a(n)。
求出其中最长的上升序列(LIS)的长度。 */

//【线性DP】【匹配转移】
//状态:f[i]表示以a[i]为结尾的、最长上升子序列的长度。
//方程:f[i]=max{ f[j]+1 (j=f[i];j--) 
        //↑↑↑【剪枝】j>=f[i]:如果j小于目前长度,不可能使答案更新
            if(a[j]

【例题2】caioj 1068 最长公共子序列

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1068】最长公共子序列
给出两个字符串 S1 和 S2 ,求它们最长公共子序列的长度。  */

//【线性DP】【匹配转移】
//状态:f[i][j]表示前缀子串s[1~i]与t[1~j]的最长公共子序列长度。
//方程:f[i][j]=max{ f[i-1][j], f[i][j-1], f[i-1][j-1]+1(if s[i]=t[j]) };
//边界:f[i][0]=f[0][j]=0; 目标:f[n][m];

const int maxn=1009;
char s[maxn],t[maxn]; //两个字符串
int f[maxn][maxn]; //前缀子串s[1~i]与t[1~j]的最长公共子序列长度

int main(){
    scanf("%s%s",s,t);
    int lens=strlen(s),lent=strlen(t);
    memset(f,0,sizeof(f));
    for(int i=1;i<=lens;i++)
        for(int j=1;j<=lent;j++){
            f[i][j]=max(f[i-1][j],f[i][j-1]);
            if(s[i-1]==t[j-1]) //如果元素公共
                f[i][j]=max(f[i][j],f[i-1][j-1]+1); //匹配字符成功
        }
    printf("%d\n",f[lens][lent]);
    return 0;
}

【例题3】洛谷 p1216 数字三角形

  • [ 数字三角形 ] 问题的特点:具有大量重叠子问题。
#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【洛谷p1216】数字三角形
一个三角形,每一步可以走到左下方的点也可以到达右下方的点。
查找从最高点到底部任意处结束的路径,使路径经过数字的和最大。 */

//【线性DP】【路径转移】
//状态:f[i][j]表示从左上角走到第i行第j列,得到的最大的和。
//方程:f[i][j]=a[i][j]+max{f[i-1][j],f[i-1][j-1](if j>1)};
//边界:f[1][1]=a[1][1];
//目标:max{f[N][j]};

int f[1009][1009],a[1009][1009];

int main(){
    int n,maxx=0; scanf("%d",&n);
    memset(f,0,sizeof(f));
    for(int i=1;i<=n;i++)
        for(int j=1;j<=i;j++){
            scanf("%d",&a[i][j]);
            f[i][j]=a[i][j];
        }
    for(int i=1;i<=n;i++)
        for(int j=1;j<=n;j++){
            if(j>1) f[i][j]+=max(f[i-1][j],f[i-1][j-1]);
            else f[i][j]+=f[i-1][j];
        }
    for(int j=1;j<=n;j++) 
        maxx=max(maxx,f[n][j]);
    printf("%d\n",maxx);
    return 0;
}

【例题4】poj 2279 Picture Permutations

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【poj2279】合影
有n个学生,站成k排,给出每排的人数,
要求每排身高从左往右递增,每列身高从前往后递增。
问有多少种排列方式。 */

//【线性DP】【匹配转移】【方案数DP】
//状态:f[a1,a2,a3,a4,a5]表示每排站了a1,a2,a3,a4,a5个人的方案数。
//方程:当a1a2时,f[a1,a2+1,a3,a4,a5]+=f[a1,a2,a3,a4,a5];
//边界:f[0,0,0,0,0]=1; 目标:f[N1,N2,N3,N4,N5];

long long f[31][31][31][31][31]; //会MLE 
int lim[6]; //每排的人数

int main(){
  int n,k; while(cin>>n&&n) {
    memset(f,0,sizeof(f)); 
    memset(lim,0,sizeof(lim));
      for(int i=1;i<=n;i++) cin>>lim[i];
      f[0][0][0][0][0]=1;
      for(int i=0;i<=lim[1];i++)
        for(int j=0;j<=lim[2];j++)
          for(int k=0;k<=lim[3];k++)
            for(int a=0;a<=lim[4];a++)
              for(int b=0;b<=lim[5];b++){
                if(i+1<=lim[1]) 
                  f[i + 1][j][k][a][b]+=f[i][j][k][a][b];
                if(j+1<=lim[2]&&j

【例题5】最长公共上升子序列(LCIS)

  • 给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
  • 求出它们的最长上升公共子序列的长度。
a[] data:
5
1 4 2 5 -12

b[] data:
4
-12 1 2 4

LCIS is 2
LCIS 所含元素为 1 4

1.确定状态

dp[i][j] 表示以 a 数组的前 i 个整数与 b 数组的前 j 个整数以 b[j] 为结尾构成的公共子序列长度。

需要注意,以 b[j] 结尾构成的公共子序列的长度不一定是最长公共子序列的长度!

2.确定状态转移方程

(1)当 a[i] == b[j] 时,我们只需要在前面找到一个能将 b[j] 接到后面的最长的公共子序列

之前最长的公共子序列在哪呢?首先我们要去找的 dp[ ][ ] 的第一维必然是 i - 1 。

第二维呢?那就需要枚举 b[1]...b[j-1] 了,因为你不知道这里面哪个最长且哪个小于 b[j]

可不可能不配对呢?也就是在 a[i] == b[j] 的情况下,需不需要考虑 dp[i][j] == dp[i-1][j] 的决策呢?

答案是不需要。如果 b[j] 不和 a[i] 配对,就是和之前的 a[1]...a[j-1] 配对,必然没有和 a[i] 配对优越。

(b[j] 和 a[i] 配对之后转移为 max(dp[i-1][k])+1,和前面的 i1 配对则是 max(dp[i1-1][k])+1 。)

(2)当 a[i] != b[j] 时, 因为 dp[i][j] 是以 b[j] 为结尾的LCIS,

如果 dp[i][j] > 0,那么就说明 a[1]..a[i] 中必然有一个整数 a[k] 等于 b[j] 。

因为 a[k] != a[i] ,那么 a[i] 对 dp[i][j] 没有贡献,于是我们不考虑它照样能得出 dp[i][j] 的最优值。

所以在 a[i] != b[j] 的情况下必然有 dp[i][j] == dp[i-1][j]

  • 综上所述,我们可以得到状态转移方程:
  • ① a[i] != b[j], dp[i][j] = dp[i-1][j]。
  • ② a[i] == b[j], dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k])。
#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/* 最长公共上升子序列(LCIS)
给出有 n 个元素的数组 a[ ],m 个元素的数组 b[ ] 。
求出它们的最长上升公共子序列的长度。 */

//【线性DP】【匹配转移】
//状态:dp[i][j]表示【a的前i个】与【b的前j个】且【以b[j]为结尾】构成的LCIS长度。
//方程:① a[i]!=b[j],dp[i][j]=dp[i-1][j]。
//      ② a[i]==b[j],dp[i][j]=max(dp[i-1][k]+1) (1<=k<=j-1 && b[j]>b[k])。
//边界:dp[0][i]=0,dp[i][0]=0;
//目标:中间过程中保存更新的答案ans。
//复杂度:O(N * M^2)

int a[3005],b[3005],dp[505][505]={0};

int lengthOfLCIS(int *a,int n,int *b,int m) {
    int ans=0,max_dp=0; //max_dp记录上一个LCIS长度最大值
    for(int i=0;ib[k] && max_dp<=dp[i][k+1])
                        max_dp=dp[i][k+1]; //记录上一个LCIS长度最大值
                dp[i+1][j+1]=max_dp+1; //此处也能匹配
            }
            ans=ans>dp[i+1][j+1]?ans:dp[i + 1][j + 1]; //更新ans
        }
    return ans; //复杂度:O(N * M^2)
}

int main(){
    int n,m,T; cin>>T;
    while(T--){
        scanf("%d",&n);
        for(int i=0;i

3.可以发现,这是O(N * M^2) 算法。

【优化1】O(N * M * log(M))算法

分析: a[i]==b[j]时在前面找到一个能将 b[j] 接到后面的最大长度的过程,是可以优化的。

设置辅助数组 len[ i ] = value,代表【长度为 i 的所有LCIS】中、【最后一位数字的最小值】为value。

Q: 为什么要这样设计辅助数组 len[] ?

需要优化的过程是一个查找过程( dp[i][j] = max(dp[i-1][k]+1) (1 <= k <= j-1 && b[j] > b[k]) )

实际上,这个查找过程是在一个一维数组中查找满足条件 b[j] > b[k] 的最大值

可以采用hash或者是二分将查找问题降到一个可以接受的复杂度,大体判断二分似乎更适合。

由于二分查找的对象必须具有单调性,而在最长公共上升子序列中,

所以的子序列是具有单调性的,所以我们可以将子序列的结尾数字作为数组中的值。

维护一个最小值,这样才能够优化判断 b[j] 到底能接在哪个位置。

Q: len[] 更新策略?

用二分查找找到 b[j] 能“嫁接”到之前子序列中的最长长度。

( ind是二分出来的len[]数组下标,根据上面的定义,ind就是以b[j]结尾的最长公共上升子序列长度 )。

a[i] == b[j],这时候 b[j] 将“嫁接”到之前子序列中的合适位置,同时更新 len[ind] = b[j] 。

a[i] != b[j],我们也需要更新 len[] 的信息,因为后面的更新需要前面的数据。

当 b[j] 与 a[i] 适配,需要查看 b[j] 与前 i - 1个数字所构成的以 b[j] 为结尾数字的最长的公共子序列。

如果有多个跟 dp[ i ][ j + 1 ]相同长度的子序列,如果比 d[j] 小则更新 len[dp[i][j + 1]]的值。

// 复杂度 O(N * M * log(M))

int binary_search(int *len, int length, int value) {
    int l = 0, r = length - 1, mid;
    while (l < r) {
        mid = (l + r) >> 1;
        if (len[mid] >= value) r = mid; 
        else l = mid + 1;
    }
    return l; //满足b[j]>len[LCIS长度]的最大LCIS长度
}

int lengthOfLCIS(int *a, int n, int *b, int m) {
    int ans = 0;
    int dp[505][505] = {0};
    int len[505];
    for (int i = 0 ; i < n ; ++i) {
        len[0] = INT_MIN;  
        for (int j = 0 ; j < m ; ++j) {
            len[j + 1] = INT_MAX;
            int ind = binary_search(len, j + 2, b[j]);
            if (a[i] != b[j]) {
                if (b[j] < len[dp[i][j + 1]])  
                // 只有当b[j]的值小于当前len[dp[i][j + 1]]的最小值时才能更新
                    len[dp[i][j + 1]] = b[j];
                dp[i + 1][j + 1] = dp[i][j + 1];
            } else {
                dp[i + 1][j + 1] = ind;
                len[ind] = b[j];
            }
            ans = ans > dp[i + 1][j + 1] ? ans : dp[i + 1][j + 1];
        }
    }
    return ans;
}

其他优化请戳 这里 ...


【例题6】三个字符串的最长公共子序列

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【caioj 1073】三个字符串的最长公共子序列
给出三个字符串,求它们最长公共子序列。 */

//【线性DP】【匹配转移】【多维DP】
//状态:a[i][j][k]表示第一个字符串的前i个字符,第二个字符串的前j个字符和
//      第三个字符串的前k个字符的最长公共子序列的长度。

const int MAXN = 101;
int a[MAXN][MAXN][MAXN];

//↓↓↓函数返回三个字符串的最长公共子序列的长度
int commonLen(string strA, string strB, string strC){
    
    int n1 = strA.size();
    int n2 = strB.size();
    int n3 = strC.size();

    for(int i=0;i<=n1;i++) //初始化:某串长度为0的情况
        for(int j=0;j<=n2;j++)
            for(int k=0;k<=n3;k++)
                if(i==0||j==0||k==0) a[i][j][k]=0;

    for(int i=1;i<=n1;i++)
        for(int j=1;j<=n2;j++)
            for(int k=1;k<=n3;k++){
                if(strA[i-1]==strB[j-1]&&strB[j-1]==strC[k-1])
                    a[i][j][k]=a[i-1][j-1][k-1]+1;
                else a[i][j][k]=max(max(a[i-1][j][k],a[i][j-1][k]),a[i][j][k -1]);
            }
    
    return a[n1][n2][n3];
}
 
int main(){
    string str1, str2, str3;
    while(cin >> str1 >> str2 >> str3)
        cout << commonLen(str1, str2, str3) << endl;
    return 0;
}

【例题7】poj 3666 Making the Grade

  • 给出长度为n的整数数列,每次可以将一个数加1或者减1,
  • 最少要多少次可以将其变成不严格的单调递增或者单调递减。

【分析】首先要证明ans最小化的情况下,一定存在一种构造方式,

使得b数组在a数组中全部出现过。证明方法简述:

记原来的数从小到大排序后分别是a1 a2 a3...an,修改后分别是b1 b2 b3...bn。

为了方便描述,在数轴上标出这些点,称为关键点。

  • 假设存在as
  • 情况一:如果这些b都相等,那么把这些b都改成as或者as+1,肯定会有一种更优。
  • 情况二:如果不全相等,那么肯定存在bp bp+1 bp+2...bq,他们的值相等,
  • 那么把他们移到左边的关键点或者右边的关键点,肯定有一种会更优。
  • 不断这样移动,最后就变成情况一了。

综上至少存在一种最优方案,最终的所有数都是原来的某个数。

最优解与两个参数有关:当前位置处理后的最大值mx,和当前情况的处理代价值cost。

显然最大值mx最小最好(这样第i+1个值花费代价较小),cost也是最小最好。

用dp[i][j]表示:前i个数构成的序列,这个序列最大值为j,dp[i][j]的值代表相应的cost。

状态转移方程:dp[i][j]=abs(j-w[i])+min(dp[i-1][k]); (k<=j)

这个表格是根据转移方程写出来的 dp 数组。

右边没填充的是因为填充的数字肯定比前面的数字大,无用,

因此,在求 min( dp[ i - 1 ][ k ] ) 时,既然右边更大、则无需考虑。

又从表格中可以看出:这里的 k 无需从1遍历到 j 。

只要在对 j 进行 for 循环的时候不断更新一个 dp[ i - 1 ][ j ] 的最小值:

mn=min(mn,dp[i-1][j]),则 dp[ i ][ j ]=abs( j - w[ i ] ) + mn 。

这样改进之后即可从本来的时候时间复杂度O(NMM)改进为O(NM);

【优化】用离散化思想改进,因为N=2000。远小于 A[ i ] 的最大值。

离散化:将序列排序一下,然后用位置的前后关系来制定其值,这样时间复杂度变成O(N^2)。

#include  //poj禁用
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【poj 3666】Making the Grade
给出长度为n的整数数列,每次可以将一个数加1或者减1,
最少要多少次可以将其变成不严格的单调递增或者单调递减。 */

//【线性DP】【匹配转移】
//关联因素:当前位置编号(当前最大值编号)mx,和当前情况的代价值cost。
//状态:dp[i][j]表示前i个数,当前最大值编号为j时,相应最小的cost。
//方程:dp[i][j]=abs(j-a[i])+min(dp[i-1][k]); (k<=j)
//优化:for循环记录min(dp[i-1][k]),即mn=min(mn,dp[i-1][j])。
//边界:dp[0][i]=0; 目标:min{dp[n][i]};
//复杂度:O(N^2)(记录mn+离散化之后)

const int N=2005;
int n,a[N],b[N];
long long int dp[N][N];

void solve(){
    for(int i=1;i<=n;i++){
        ll mn=dp[i-1][1];
        for(int j=1;j<=n;j++){
            mn=min(mn,dp[i-1][j]);
            dp[i][j]=abs(a[i]-b[j])+mn;
        }
    }
    long long ans=dp[n][1];
    for(int i=1;i<=n;i++)
        ans=min(ans,dp[n][i]);
    printf("%lld\n",ans);
}

int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]), b[i]=a[i]; //b[i]用于离散化
    sort(b+1,b+n+1); //***注意:此处不需要去重***
    solve(); return 0;
}

【例题8】noip 2008 传纸条

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

/*【洛谷 P1006】传纸条
给出M*N(1<=M,N<=50)的矩阵,在这个矩阵内找出两条从1,1到m,n的路径。
(一条从1,1到m,n;一条从m,n到1,1),且路径之上的权值和k最大。
PS:(1,1)和(m,n)权值为0。 */

const int maxn=60;
int a[maxn][maxn];

int F[2*maxn][maxn][maxn]; 
/* 第一维度维护的是纵坐标与横坐标的和。
   =>在同一斜线上 =>结合纵坐标就可以找到确切位置。
   第二维度维护的是相对在左边的点的纵坐标。
   第三维度维护的是相对在右边的点的纵坐标。*/

//F[sum][i][j]=max{F[sum-1][i][j],F[sum-1][i][j-1],F[sum-1][i-1][j],F[sum-1][i-1][j-1]};

int main(){
    int m,n; scanf("%d%d",&m,&n);
    for(int i=1;i<=m;i++)
        for(int j=1;j<=n;j++) scanf("%d",&a[i][j]);
    memset(F,-1,sizeof(F));//赋初值为-1 
    F[2][1][1]=0; //最初的点,在左上角,好感度为0 [初始化]
    for(int k=3;ks) s=F[k-1][i][j];
                if(F[k-1][i-1][j]>s) s=F[k-1][i-1][j];
                if(F[k-1][i][j-1]>s) s=F[k-1][i][j-1];
                if(F[k-1][i-1][j-1]>s) s=F[k-1][i-1][j-1];
                if(s==-1) continue; 
                //↑↑↑当s为-1时,说明四种情况都不能到该点,故不存在
                F[k][i][j]=s+a[k-i][i]+a[k-j][j];
                //↑↑↑该点的值为最大的前一个值与当前F[k][i][j]表示两点的值的和
            }
    printf("%d",F[m+n-1][n-1][n]); //因为i永远小于j,所以右下角的点不会求到
    //但是到右下角只有一种情况,在右下角的上面和右下角的左边,直接输出即可
    return 0;
} 

【例题9】tyvj 1061 移动服务 戳这里

【例题10】SGU 167 I - country 戳这里

【例题11】Cookies 看进阶指南吧Σ( ° △ °|||)

 

二. 背包DP

背包问题的经典讲解可以参见背包详解,还有背包六问

 

(1)0/1背包

问题(0/1背包): 有n个体积和价值分别为vi和wi的物品。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

0/1背包:常用于方案数DP,最优值DP。

思想:对于每个物品,比较 [ 放的价值大 ] 还是 [ 不放的价值大 ] 。

转换成方程: f [ i ] [ j ] = Max{ f [ i − 1 ] [ j ] ,f [ i − 1 ] [ j −v [ i ] ] + w [ i ] } ;

 

【思路1】初步分析

//【背包DP】【01背包】初步分析
//状态:dp[i][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i][j]=max{dp[i-1][j](不选i),dp[i-1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n][j]}(0<=j<=m); 

memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++) dp[i][j]=dp[i-1][j];
    for(int j=v[i];j<=m;j++) //注意:从v[i]开始
        dp[i][j]=max(dp[i][j],dp[i-1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n][j]);

 

【思路2】滚动数组优化空间

//【背包DP】【01背包】滚动数组
//优化:滚动数组优化,降低空间开销。
//实现:把阶段i的状态存储在第一维下标为i&1的二维数组中。
//当i为奇数时,i&1=1; 当i为偶数时,i&1=0。
//DP的状态相当于在dp[0][],dp[1][]两个数组中交替转移,节省空间。
//状态:dp[i&1][j]表示前i个物品、选择体积和为j的、所得的最大价值和。
//方程:dp[i&1][j]=max{dp[(i-1)&1][j](不选i),dp[(i-1)&1][j-vi]+wi(选i)};
//初值:dp[0][0]=0; 其余均为负无穷。
//目标:max{dp[n&1][j]}(0<=j<=m); 

int dp[2][max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0][0]=0;
for(int i=1;i<=n;i++){
    for(int j=0;j<=m;j++) dp[i&1][j]=dp[(i-1)&1][j];
    for(int j=v[i];j<=m;j++) //注意:从v[i]开始
        dp[i&1][j]=max(dp[i&1][j],dp[(i-1)&1][j-v[i]]+w[i]);
}
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[n&1][j]);

 

【思路3】转化为一维

转换成一维方程,需要  逆序 

逆序的目的是:保证后一个 f [ v ] 和 f [ v - wei [ i ] ] + val [ i ] 是继承前一状态的。

//【背包DP】【01背包】转化为一维
//优化:进一步省略掉dp数组的第一维。
//状态:dp[j]表示放入背包的体积和为j、所得的最大价值和。
//方程:dp[j]=max{dp[j],dp[j-vi]+wi};
//初值:dp[0]=0; 其余均为负无穷。
//目标:max{dp[j]}(0<=j<=m); 

/*【注意】一定要倒序循环来保证唯一性。循环到j时:
1.dp数组的后半部分dp[j~m]处于“第i个阶段”,已经考虑过第i个物品。
2.前半部分dp[0~j-1]处于“第i-1个阶段”,还没有考虑第i个物品。
接下来j不断减小,意味着总是用“第i-1个阶段”状态来向“第i个阶段”转移,
符合线性DP的原则,进而保证了第i个物品只会被放入背包一次。
如果用正序,假设dp[j]被dp[j-vi]+wi更新,接下来j增大到j+vi时,
dp[j+vi]又可能会被dp[j]+wi更新,这样,第i个物品就用了两次。*/

int dp[max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0]=0;
for(int i=1;i<=n;i++)
    for(int j=m;j>=v[i];j--) //注意:一定要倒序循环
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

 

【例题】poj 1015  Jury Compromise

#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;

/*【poj 1015】Jury Compromise
从n个人中选m人组成陪审团。办法是:两个人根据对它们的喜欢程度打分。
分值从0到20。选出的m个人的得分中,必须满足第一人总分D和第二人总分P的差的绝对值|D-P|最小。
如果有多种选择方案的|D-P|值相同,那么选两人总分之和D+P最大的方案。
输出:这m个人的第一人总值D和第二人总值P,并升序输出m人的编号。 */

/*【多维度的0/1背包】思路1:f数组记录可行性。【可行性DP】
把n个候选人看成n个物品,每个物品有如下三个需考虑的因素:
1.“人数”,每人的“人数”为1,要填满容量m的背包。
2.“第一人得分”,即第一人给该候选人打的分数a[i]。
3.“第二人得分”,即第二人给该候选人打的分数b[i]。
循环考虑每个候选人的入选情况,循环到阶段i时,
表示已经考虑了前i人的入选情况,用f[j][d][p]表示已有j人入选时、方案可行性。
方程:f[j][d][p]=f[j][d][p] or f[j-1][d-a[i]][p-b[i]] ;
初值:f[0][0][0]=true; 其余均为false。
目标:某个状态f[m][d][p]=true,且|d-p|最小。|d-p|相同时d+p最大。 */

/*【多维度的0/1背包】思路2:第二维度记录d-p,f数组记录d+p的最大值。
状态:循环到第i个人时,f[j][k]表示已有j人入选、且d-p=k时,d+p的最大值。
方程:f[j][k]=max{ f[j][k] , f[j-1][k-a[i]+b[i]]+a[i]+b[i] };
初值:f[0][k]=0; 其余均为负无穷。
目标:某个状态f[m][k]=true,且|k|最小。|k|相同时f[m][k]最大。 */

//【注意】k=d-p,k可能为负数。而程序中数组下标不能为负数。
//不妨在程序中将辩控差的值都加上修正值fix=20*m,回避下标负数问题。
//区间整体平移,从[-20*m,20*m]映射到[0,20*m*2]。
//此时初始条件修正为dp(0,20*m)=0,其他均为-1。 

//【注意】第一维度中,每个候选人只会被选一次,所以要用倒序循环。

//本题还需要记录入选的具体方案,可以采用【记录转移路径】的方法。
//即:额外建立数组d[j][k]记录状态f[j][k]的最大值是最后选了哪一人得到的。
//求出最优解后,沿着d数组记录的路径进行转移:不断从f[j][k],
//递归到 f[ j - 1 ][ k - a[d[j][k]] + b[d[j][k]] ] ,直到达到f[0][0]。

const int maxn=811,maxm=31;
int n,m,t=0,a[maxn],b[maxn],id[maxm];
int f[maxm][maxn],d[maxm][maxn];

bool write(int x,int y,int z){ //判断路径能否到达(能否选择z)
  //(路径满足的条件:z是还没有被选择的人)
  //d[x-][y-]某一次值为z,即路径上有z,不成立。
  //这种情况下x不能减小到0,返回false。
  while(x&&d[x][y]!=z){ y-=a[d[x][y]]-b[d[x][y]]; x--; }
  return !x; //x=0时才返回true
}

int main(){
  while(scanf("%d%d",&n,&m) && n && m){ //输入0 0停止

    memset(d,0,sizeof(d));
    memset(f,-1,sizeof(f)); //初始化为负值
    //memset(f,127,sizeof(f));会得到无穷大
    f[0][m*20]=0; //初始化

    for(int i=1;i<=n;i++) 
      scanf("%d%d",&a[i],&b[i]);
    for(int i=1;i<=m;i++)
      for(int j=0;j<=m*40;j++)
        if(f[i-1][j]>=0) //之前的这个状态存在【可行性】
          for(int k=1;k<=n;k++){
            if(f[i][j+a[k]-b[k]]f[m][m*20-k1]?m*20+k1:m*20-k1; //询问正负
      
    printf("Best jury has value %d for prosecution and value %d for defence:\n",
              (ans+f[m][ans]-m*20)>>1,(f[m][ans]-ans+m*20)>>1); //输出
    //f[m][ans]=d+p,ans-m*20=|k1|=|d-p|,用此来求d、p
    
    for(int i=1,j=m,k=ans;i<=m;i++){
      id[i]=d[j][k]; k-=a[d[j][k]]-b[d[j][k]]; j--;
    } //id[i]用于记录方案
    
    sort(id+1,id+1+m); //按编号顺序
    for(int i=1;i<=m;i++) printf(" %d",id[i]);
    printf("\n\n");
  }
  return 0;
}

 

【 0/1 背包练习题】

  • HDU2546:饭卡
  • HDU1171:BigEvent in HDU
  • HDU2602:BoneCollector (模板题)
  • HDU2639:BoneCollector II(01背包第k优解)
  • HDU2955:Robberies
  • HDU3466:ProudMerchants
  • HDU1864:最大报销额

 

(2)完全背包

问题(完全背包): 有n种体积和价值分别为vi和wi的物品,并且有无数个。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

完全背包的特点:物品有【无数个】。

思想:对于每个物品,比较 [ 放几个的价值大 ] 。

转换成方程: f [ i ] [ j ] = Max { f [ i − 1 ] [ j - k * v [ i ] ] + k * w [ i ] (0 <= k * v [ i ] <= j) };

 

【具体实现】转化为一维

//【背包DP】【完全背包】转化为一维
//优化:进一步省略掉dp数组的第一维。
//状态:dp[j]表示放入背包的体积和为j、所得的最大价值和。
//方程:dp[j]=max{dp[j],dp[j-k*vi]+k*wi};
//初值:dp[0]=0; 其余均为负无穷。
//目标:max{dp[j]}(0<=j<=m);

//【注意】根据01背包的逆序循环提示,完全背包要用正序循环。
//正序循环对应着:每种物品可以使用无限次。

int dp[max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0]=0;
for(int i=1;i<=n;i++)
    for(int j=v[i];j<=m;j++) //注意:一定要正序循环
        dp[j]=max(dp[j],dp[j-v[i]]+w[i]);
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

 

【例题】TYVJ 1172

//【tyvj 1172】把正整数N拆分成若干正整数相加的形式,
// 求方案数 mod 2147483648 的结果。

unsigned int dp[max_m+1]; //unsigned表示无符号
memset(dp,0,sizeof(dp));
dp[0]=1; //起点
for(int i=1;i<=n;i++)
    for(int j=i;j<=n;j++) //每个值,都可以有前面的任意一个值得到
        dp[j]=(dp[j]+dp[j-i]) % 2147483648u; //注意这个数后面有个u
cout<<(dp[n]>0 ? dp[n]-1 : 2147483647)<

 

【完全背包练习题】

  • Hdu 1114: http://acm.hdu.edu.cn/showproblem.php?pid=1114
  • Hdu 1248: http://acm.hdu.edu.cn/showproblem.php?pid=1248
  • Hdu 2159: http://acm.hdu.edu.cn/showproblem.php?pid=2159

 

(3)多重背包

问题(多重背包):
有n种重量和价值分别为vi和ci的物品,每种物品有si个。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。

多重背包的特点:多维度,有限制存在。

转换成方程:f [ i ][ j ] = max{ f [ i - 1 ][ j - k*v[i] ] + k*w[i] | 0 <= k <= s[i] };

 

【思路1】直接拆分法

//【背包DP】【多重背包】直接拆分法
//概述:把第i种物品看成独立的s[i]个物品,转化为0/1背包。
//状态:dp[j]表示放入背包的体积和为j时、所得的最大价值和。
//方程:dp[j]=max{dp[j],dp[j-vi]+wi}; 此时的i=sumi。
//初值:dp[0]=0; 其余均为负无穷。
//目标:max{dp[j]}(0<=j<=m);

//【注意】01背包要用逆序循环。

int dp[max_m+1];
memset(dp,0xcf,sizeof(dp)); //-INF
dp[0]=0;
for(int i=1;i<=n;i++)
  for(int j=1;j<=s[i];j++) //【与分组背包不同】
  //↑↑↑这里用到了“转移累积”的方法,便于价值更新
    for(int k=m;k>=v[i];k--) //倒序循环
      dp[k]=max(dp[k],dp[k-v[i]]+w[i]);
int ans=0;
for(int j=0;j<=m;j++) ans=max(ans,dp[j]);

 

【思路2】二进制拆分法

  • 思想:将每种物品拆成 O(logS[i]) 个。
  • 引入:2^0,2^1,...,2^(k-1)中选若干个相加,可以表示0~2^k-1的任何整数。
  • 概述:把第i种物品看成独立的p+2个物品,转化为0/1背包。

1.对于每个物品i,求出满足2^0+2^1+...+2^p<=s[i]的最大p。

2.设r[i]=s[i]-(2^0+2^1+...+2^p)。r[i]用于辅助拆分。

   根据p的最大性,2^0+2^1+...+2^(p+1)>s[i],那么2^(p+1)>r[i]。

   所以,2^0,2^1,...,2^p中选若干个相加,可以表示0~r[i]的任何整数。

3.从2^0,...,2^p,r[i]中选若干个相加,可以表示r[i]~r[i]+2^(p+1)-1的任何整数。

   r[i]=s[i]-(2^0+...+2^p),所以r[i]+2^(p+1)-1=s[i]。即区间变为r[i]~s[i]。

4.这样,我们可以把数量为s[i]的第i种物品拆分成p+2个物品:

   2^0*v[i],2^1*v[i],...,2^p*v[i],r[i]*v[i]。

   这p+2个物品刚好可以凑成0~s[i]*v[i]之间所有v[i]的倍数。

   这等价于原问题中:体积为v[i]的物品可以使用0~s[i]次。


【概念实现性代码】(小数据能实现,大数据wa QAQ)

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

//【背包DP】【多重背包】二进制拆分法

/*【思想分析】每种物品拆成 O(logS[i]) 个。
引入:2^0,2^1,...,2^(k-1)中选若干个相加,可以表示0~2^k-1的任何整数。
1.对于每个物品i,求出满足2^0+2^1+...+2^p<=s[i]的最大p。
2.设r[i]=s[i]-(2^0+2^1+...+2^p)。r[i]用于辅助拆分。
  根据p的最大性,2^0+2^1+...+2^(p+1)>s[i],那么2^(p+1)>r[i]。
  所以,2^0,2^1,...,2^p中选若干个相加,可以表示0~r[i]的任何整数。
3.从2^0,...,2^p,r[i]中选若干个相加,可以表示r[i]~r[i]+2^(p+1)-1的任何整数。
  r[i]=s[i]-(2^0+...+2^p),所以r[i]+2^(p+1)-1=s[i]。即区间变为r[i]~s[i]。
4.这样,我们可以把数量为s[i]的第i种物品拆分成p+2个物品:
  2^0*v[i],2^1*v[i],...,2^p*v[i],r[i]*v[i]。
  这p+2个物品刚好可以凑成0~s[i]*v[i]之间所有v[i]的倍数。
  这等价于原问题中:体积为v[i]的物品可以使用0~s[i]次。 */

//概述:把第i种物品看成独立的p+2个物品,转化为0/1背包。

const int max_m=109,max_n=109;
int n,m,dp[max_m],init[30]; //init记录2^i
int v[max_n],w[max_n],s[max_n],p[max_n],c[max_n][31]; 
//s为每样的个数,p为二进制分解的个数(p+2),c存储p+2个物品对应的i的个数

void pre_binary(){ //预处理2^i
  init[0]=1; for(int i=1;i<=30;i++) init[i]=init[i-1]*2;
}

void do_binary(int i){ //二进制拆分
  int p1=0,ss=s[i]; //r[i]=s[i]-(2^0+2^1+...+2^p)=ss;
  while(ss-init[p1]>=0){ ss-=init[p1]; p1++; }
  p[i]=p1+1; //这里的p[i]即是p+2
  for(int ii=0;ii=v[i];k--) //倒序循环
          dp[k]=max(dp[k],dp[k-c[i][j]*v[i]]+c[i][j]*w[i]);

    int ans=0; for(int j=0;j<=m;j++) ans=max(ans,dp[j]);
    printf("%d\n",ans);
  }
  return 0;
}

【AC代码】(以 hdu 2191 为例)

#include 
using namespace std;
typedef long long ll;
typedef unsigned long long ull;

int p[100],h[210],c[100]; //价值  重量  袋数
int pp[2000],hh[2000],cc[2000],dp[2000];

int main(){
    int n,m,t; scanf("%d",&t);
    while(t--){
        int k=0; //记录p
        memset(dp,0,sizeof(dp));
        scanf("%d%d",&n,&m);
        for(int i=0;i0){ //有r[i]存在
                pp[k]=c[i]*p[i];
                hh[k++]=c[i]*h[i];
            }
        }
        for(int i=0;i=pp[i];j--)
               dp[j]=max(dp[j],dp[j-pp[i]]+hh[i]);
        printf("%d\n",dp[n]);
    }
    return 0;
}

 

【例题】poj 1742 Coins

  • 给你n种面值的硬币,面值为a1...an,数量分别为c1...cn,
  • 问,这些硬币的组合之后,在1~m之间能拼成的面值数。

【关键词】多重背包+可行性dp(不用求最优性)。


【思路1】背包DP的直接拆分法

状态:在循环进行到 i 时,f [ j ] 表示前 i 种硬币能否拼成面值 j 。

//背包DP的直接拆分法
bool f[100010];
memset(f,0,sizeof(f)); f[0]=true;
for(int i=1;i<=n;i++)
    for(int j=1;j<=c[i];j++)
        for(int k=m;k>=a[i];k--){ //如果前方有一种情况是true,f[k]即为true(=1)
            f[k]|=f[k-a[i]]; // a|=b:把a和b做按位或(|)操作,结果赋值给a
int ans=0; for(int i=1;i<=m;i++) ans+=f[i];

【思路2】巧用贪心策略

前 i 种硬币能够拼成面值 j,只有两类可能情况:

  1. 前 i - 1 种硬币能够拼成面值 j,即 i 阶段之前 f [ j ] 已经为TRUE。
  2. 拼成 j 的过程中,使用了第 i 种硬币,递推寻找 f [ j - a [ i ] ] = TRUE。

贪心策略:used [ j ] 表示 f [ j ] 在阶段 i 时为TRUE,至少需要多少个第 i 种硬币。

对应第一种情况,used [ j ] = 0; 对应第二种情况,used [ j ] = used [ j - a[ i ] ] + 1 。

//贪心策略的实现函数
int used[100010];
for(int i=1;i<=n;i++) {
    for(int j=0;j<=m;j++) used[j]=0; //初始化
    for(int j=a[i];j<=m;j++)
        if(!f[j] && f[j-a[i]] && used[j-a[i]]

【总代码实现】

#include 
#include 
#include 
#include 
#include 
#include  
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;

/*【poj 1742】Coins
给你n种面值的硬币,面值为a1...an,数量分别为s1...sn,
问:这些硬币的组合之后,在1~m之间能拼成的面值数。*/

int f[100010],used[100010];
int a[110],s[110],n,m,ans;

int main(){
    while(cin>>n>>m && n!=0){
        for(int i=1;i<=m;i++) f[i]=0; //dp数组初始化
        for(int i=1;i<=n;i++) cin>>a[i];
        for(int i=1;i<=n;i++) cin>>s[i];
        f[0]=1; //dp起点
        for(int i=1;i<=n;i++){
            for(int j=0;j<=m;j++) used[j]=0; //used[i]初始化
            for(int j=a[i];j<=m;j++)
                if(!f[j] && f[j-a[i]] && used[j-a[i]]

 

【多重背包练习题】

  • hdu 2844:http://acm.hdu.edu.cn/showproblem.php?pid=2844
  • poj 1014:http://poj.org/problem?id=1014
  • hdu 3591:http://acm.hdu.edu.cn/showproblem.php?pid=3591
  • poj 1276:http://poj.org/problem?id=1276

 

(4)分组背包

问题(分组背包):
有n组物品,其中第i组有Ci个物品。
第i组第j个物品的体积为Vij、价值为Wij。
有一个背包,容积为m,在每组中至多选择一个物品。
求所有挑选方案中价值总和的最大值。

引入:分组背包是许多树形DP状态转移的基本模型。

状态:f [ i ][ j ] 表示前 i 组中,选出总体积为 j 的物品放入背包,物品的最大价值和。

方程:f [ i ][ j ] = max{ f [ i - 1 ][ j ],f [ i - 1 ][ j - v[i][k] ] + w[i][k] | 1 <= k <= c[i] };

注意:1. 省略掉 f 数组的第一维(阶段),用 j 的倒序循环进行操作。

           2. 控制 “阶段 i ” 的状态只能从 “阶段 i - 1” 转移而来。


分组背包的代码实现过程:

//【背包DP】【分组背包】
memset(f,0xcf,sizeof(f)); //负无穷
f[0]=0; //dp起点
for(int i=1;i<=n;i++)
  for(int j=m;j>=0;j--) //倒序
    for(int k=1;k<=c[i];k++) //【与多重背包相反】
    //↑↑↑这里的个数循环要放在容量循环的后面,才能保证唯一性
      if(j>=v[i][k]) f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);

 

【分组背包练习题】

  • hdu 1712:http://acm.hdu.edu.cn/showproblem.php?pid=1712
  • hdu 3033:http://acm.hdu.edu.cn/showproblem.php?pid=3033
  • hdu 4341:http://acm.hdu.edu.cn/showproblem.php?pid=4341

 

三. 区间DP

{ 1. 概念引入 }

以 “ 区间长度 ” 作为DP的 “ 阶段 ”,用 “ 区间左右端点 ” 描述 “ 维度 ” 。

一个状态、由若干个比它更小、且包含于它的区间、所代表的状态转移而来。

区间DP的初态一般由长度为1的 “ 元区间 ” 构成(dp[i][i] 初始化为自身的值)。

特征:能将问题分解为两两合并的形式。也可以将多个问题整合分析。

典型应用:石子合并,能量项链,凸多边形的划分等问题。

区间DP的模式可以概括为:向下划分,再向上递推。

决策:dp[i][j]=min{ dp[i][k]+dp[k+1][j] | i<=k

 区间DP的状态转移方法:

  1. 记忆化搜索。
  2. 从小到大枚举区间长度,枚举对应长度的区间。
for(int len=1;len<=N;++len) //区间长度

    for(int l=1,r=len;r<=N;++l,++r)

        { 考虑F[l][r]的转移方式 } 

 

{ 2. 例题详解 }

【例题1】洛谷 p1430 序列取数

  • 给定一个长为n的整数序列(n<=1000),由A和B轮流取数(A先取)。
  • 每个人可从序列的左端或右端取若干个数(至少一个),但不能两端都取。
  • 所有数都被取走后,两人分别统计所取数的和作为各自的得分。
  • 假设A和B都足够聪明,都使自己得分尽量高,求A的最终得分。

题目分析:

最大化A的得分=最大化(A-B)。

因为每次只能从左边取或右边取,所以剩下的一定是中间的区间。

用 d[ l ][ r ] 表示目前剩下区间为l、r时,先手可能达到的max得分。

状态转移时,我们要枚举(对该区间而言)从左还是右取,以及取多少个,

即对于断点k,剩下一个(k,j)或是(i,k)的子序列(i<=k<=j)。

再用sum[i][j]表示i~j的和,则有:

d[i][j]=sum[i][j]-min(d[i+1][j],d[i+2][j],...,d[j][j],d[i][j-2],d[i][j-1],d[i][i],0);

其中 0 表示全取完。最终答案为d[1][n]。

优化:定义 f[i][j]=min(d[i][j],d[i+1][j],d[i+2][j],...,d[j][j]);

          g[i][j]=min(d[i][j],d[i][j-1],d[i][j-2],...,d[i][i]);

那么转移方程变为:d[i][j]=sum(i,j)-min(f[i+1][j],g[i][j-1],0);

f[i][j]=min(d[i][j],f[i+1][j]); g[i][j]=min(d[i][j],g[i][j-1]);

代码实现:

#include 
using namespace std;

const int N=1005,inf=1e9;

int a[N],sum[N],d[N][N],f[N][N],g[N][N]; //用f、g数组来优化DP数组d

int read(){ //读入优化
    int x=0,f=1;char ch=getchar();
    while(ch>'9'||ch<'0'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x*=10;x+=(ch-'0');ch=getchar();}
    return x*f;
}

void print(int x){ //输出优化
    if(x<0) putchar('-'),x=-x;
    if(x>9) print(x/10); 
    putchar(x%10+'0');
}

int main(){
    int T,n; T=read();
    while(T--) {
        n=read();
        for(int i=1;i<=n;i++){
            a[i]=read(); sum[i]=sum[i-1]+a[i]; //前缀和sum数组
        }
        for(int i=1;i<=n;i++) f[i][i]=g[i][i]=d[i][i]=a[i]; //初始化边界
        for(int L=1;L<=n;L++){ //枚举长度
            for(int i=1;i<=n-L;i++){ //区间向后滚动
                int j=i+L,cnt=0; //递推部分
                cnt=min(cnt,min(f[i+1][j],g[i][j-1]));
                d[i][j]=sum[j]-sum[i-1]-cnt; 
                //↑↑↑ d[i][j]:目前剩下区间为i、j时,先手可能达到的max得分
                f[i][j]=min(d[i][j],f[i+1][j]);
                g[i][j]=min(d[i][j],g[i][j-1]);
            }
        }
        print(d[1][n]); putchar('\n');
    }
    return 0;
}

 

【例题2】洛谷 p4170 涂色

  • 假设你有一条长度为n的木版,初始时没有涂过任何颜色。
  • n=5时,想要把它的5个单位长度分别涂上红、绿、蓝、绿、红色,
  • 用一个长度为5的字符串表示这个目标:RGBGR。
  • 每次 [ 把一段连续的木版涂成一个给定的颜色 ] ,颜色可以覆盖。
  • 用尽量少的涂色次数达到目标。

【分析】

f[l][r]表示把区间[l,r]全部染成正确颜色的最小次数。

1.枚举分界点m:f[l][m]+f[m+1][r]+价值。

2.当col[l]=col[r]时:把[l,r]全部染成col[l]的颜色,
对于剩下的区间[_l,_r],再进行转移f[l][r]=f[_l][_r]+1。

3.求出_l,_r 
  1)剩余区间刚好同色,得到f[l][r]=1;
  2)枚举_l,直到第一个col[_l]!=col[l]的位置;
    枚举_r,直到第一个col[_r]!=col[r]的位置。

【求f[i][j]具体步骤】

当i==j时,子串明显只需要涂色一次,于是f[i][j]=1。

当i!=j且s[i]==s[j]时,可以想到只需要在首次涂色时多涂一格即可,
可以直接继承之前的状态,于是f[i][j]=min(f[i][j-1],f[i+1][j])。

当i!=j且s[i]!=s[j]时,我们需要考虑将子串断成两部分来涂色,
于是需要枚举子串的断点,设断点为k,那么f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])。

代码实现:

#include 
using namespace std;

char s[52];
int f[52][52];

int main() {
    int n; scanf("%s",s+1);
    n=strlen(s+1);
    memset(f,63,sizeof(f));  //初始化为较大数
    for(int i=1;i<=n;++i) f[i][i]=1; 
    for(int l=1;l

 

【例题3】洛谷 p4342 Polygon

  • n个顶点的多边形。第一步,删除其中一条边。
  • 随后n-1步:选择一条边连接的两个顶点V1和V2,
  • 用边运算符计算V1和V2,得到的结果[作为新顶点替换这两个顶点]。
  • 游戏结束时,只有一个顶点,点的值即为得分。
  • 编写一个程序,给定一个多边形,计算最高可能的分数。
【分析】

f[l,r]表示合成l,r区间内的所有顶点后得出的最值。
可此时需要记录最大值和最小值。

因为max的来源只可能是两个max相加、相乘,或两个min相乘(负负得正); 
同时,min的来源只可能是两个min相加、相乘,或一个最大值和一个最小值相乘(正负得负)。

用f[l][r][0]记录max; 用f[l][r][1]记录min。
f[l][r][0]=max{max{f[l][k][0] ‘*’or‘+’ f[k+1][r][0],f[l][k][1] * f[k+1][r][1]}}
f[l][r][1]=min{min{f[l][k][1] ‘*’or‘+’ f[k+1][r][1],
                    f[l][k][1] * f[k+1][r][0],f[l][k][0] * f[k+1][r][1]}}

初值:f[i][i][0]=f[i][i][1]=a[i],其余的设为INF。目标:f[1][N][0]。

【优化】还可以进一步优化:优化枚举第一步删除边的耗时。

任意选择删除边,[破环成链],然后把剩下的链复制一倍接在末尾。

(以被删除的边逆时针方向的第一个节点为开头,接上这个链)。

这样,我们只需要对前N个阶段进行DP,每个阶段不会超过2N个状态。

最后的答案为:max { f[ i ][ i+N-1 ][ 1 ] }。

代码实现:

#include 
using namespace std;

const int SIZE=55;
int a[SIZE<<1]; //点的数值
char op[SIZE<<1]; //边上的符号
int f[SIZE<<1][SIZE<<1][2];

int main(){
    int n; cin>>n;

    for(int i=1;i<=n;i++){ //空间开2倍,把环复制成两倍的链
        cin>>op[i]>>a[i];
        op[i+n]=op[i],a[i+n]=a[i];
    }
    for(int i=1;i<=2*n;i++){ //初始化
        for(int j=1;j<=2*n;j++){
            if(i==j) f[i][i][0]=f[i][i][1]=a[i];
            else f[i][j][1]=32768,f[i][j][0]=-32769;
        } //↑↑↑题中给出:对于任何操作,顶点数字都在[-32768,32767]的范围内
    }
    for(int i=1;i<=2*n;i++) f[i][i][0]=f[i][i][1]=a[i];

    for(int L=1;L<=n;L++) //区间长度
      for(int i=1;i<=2*n-L+1;i++){ //区间起点
        int l=i,r=i+L-1;
        for(int k=l;k

 

【例题4】洛谷 p1880 石子合并

#include 
using namespace std;
typedef long long ll;

/*【洛谷p1880】石子合并【区间DP】
在一个圆形操场的四周摆放N堆石子,现要将石子有次序地合并成一堆。
规定每次只能选相邻的2堆合并,并将新的一堆的石子数,记为该次合并的得分。
试设计出1个算法,计算出将N堆石子合并成1堆的最小得分和最大得分。*/

/*【分析】用sum[i]维护序列前缀和。
f_max[l,r]表示合并l~r堆内的所有石子后最大得分。
f_min[l,r]表示合并l~r堆内的所有石子后最小得分。
初始条件:f_max[i][j]=0; f_min[i][i]=0; f_min[i][j]=INF;
f_max[i][j]=max{f_max[i][k]+f_max[k+1][j]+sum[j]-sum[i-1]};
f_min[i][j]=min{f_min[i][k]+f_min[k+1][j]+sum[j]-sum[i-1]};*/

/*【优化】环的处理——[破环成链]
选取一处破环成链,再把链复制一倍接在末尾。
枚举f[1][N],f[2][N+1],...,f[N][2*N-1]取最优。
最后的答案为:max(或min){f[i][i+N-1]}。 */

//注意:定义变量的时候不能用fmax和fmin。

const int maxn=227,INF=0x7fffffff/2;
int f_max[maxn][maxn],f_min[maxn][maxn],s[maxn][maxn]={0};
int a[maxn],sum[maxn]={0},n,ans_max=0,ans_min=INF;

int main(){
    int n; cin>>n;
    for(int i=1;i<=n;i++){
        cin>>a[i]; a[i+n]=a[i];
    } //↑↑↑破环为链,并将链复制一遍
    for(int i=1;i<=2*n;i++){
        sum[i]=sum[i-1]+a[i];
        f_max[i][i]=0; f_min[i][i]=0;
    }
    for(int L=2;L<=n;L++) //枚举区间长
        for(int i=1;i<=2*n-L+1;i++){ //合并的起始位置
            int j=i+L-1; //推算出合并的终止位置 
            f_max[i][j]=0; f_min[i][j]=INF; 
            for(int k=i;k

 

【例题5】洛谷 p1063 能量项链

#include 
using namespace std;
typedef long long ll;

/*【洛谷p1063】能量项链【区间DP】
能量球组成的项链。相邻两球可以合并产生新球。
合并规则:如果前一颗能量珠的头标记为m,尾标记为r,
后一颗能量珠的头标记为r,尾标记为n,则聚合后释放的能量为m*r*n。
问:一条项链怎样合并才能得到最大能量?求最大能量值。 */

/*【优化】环的处理——[破环成链]
选取一处破环成链,再把链复制一倍接在末尾。
枚举f[1][N],f[2][N+1],...,f[N][2*N-1]取最优。
最后的答案为:max(或min){f[i][i+N-1]}。 */

int a[309],nextt[309],f[309][309]; //记得开两倍以上

int main(){
  int n,ans=0; cin>>n;
  for(int i=1;i<=n;i++){ cin>>a[i]; a[i+n]=a[i]; }
  //↑↑↑珠子由环拆分为链,重复存储一遍
  for(int i=1;i<=2*n-1;i++){ nextt[i]=a[i+1]; f[i][i]=0; } 
  nextt[2*n]=a[1]; //nextt[i]为i~nextt的项链,尾部的对应值
  for(int L=2;L<=n;L++) //区间长度
    for(int i=1;i<=2*n-L+1;i++){
      int j=i+L-1;
      for(int k=i;k

 

【例题6】凸多边形的划分

#include 
using namespace std;
typedef long long ll;

/*【凸多边形的划分】
具有 N 个顶点(从 1 到 N 编号)的凸多边形,每个顶点的权均已知。
问:如何把这个凸多边形划分成 N−2 个互不相交的三角形,
使得这些三角形顶点的权值乘积之和最小? */

/*【分析】将顶点按顺时针编号,可以用两个顶点描述一个凸多边形。
设 f(i,j) 表示 i j 这一段连续顶点的多边形划分后最小乘积。
枚举点 k。i、j 和 k 相连成三角形,并把原多边形划分成两个子多边形。
则有:f(i,j)=min{f(i,k)+f(k,j)+a[i]∗a[j]∗a[k]}; (1<=i可以看成多边形剖分过程,分成多个多边形一步一步进展,每一步累加即可。*/

//初态(由边来描述):f[i][i+1]=0; 目标状态:f[1][n]。

//注意:此题原意是用高精度来实现,此处只用longlong代替

ll w[100],f[60][60];

int main(){
    ll n; scanf("%lld",&n); 
    memset(f,0x3f,sizeof(f));
    for(ll i=1;i<=n;i++) scanf("%lld",&w[i]); //点权
    for(ll i=n;i>=1;i--)
        for(ll j=i+1;j<=n;j++){
            if(j-i==1) f[i][j]=0; //初态,描述边
            else if(j-i==2) f[i][j]=w[i]*w[i+1]*w[i+2]; //最小的三角形
            else for(LL k=i+1;k<=j-1;k++) //寻找中间k值,合并为大多边形
                f[i][j]=min(f[i][j],f[i][k]+f[k][j]+w[i]*w[j]*w[k]);
        }
    printf("%lld\n",f[1][n]);
    return 0;
}

 

【例题7】括号配对

#include 
using namespace std;
typedef long long ll;

/*【括号配对】
定义如下规则序列:1.空序列是规则序列;
2.如果S是规则序列,那么(S)和[S]也是规则序列;
3.如果A和B都是规则序列,那么AB也是规则序列。
由‘(’,‘)’,‘[’,‘]’构成的序列,请添加尽量少的括号,得到一个规则序列。 */

/*【分析】枚举区间i,j,dp[i][j]表示添加的最少括号数,
如果i和j处的括号能够匹配,则dp[i][j]=dp[i+1][j-1]+1; 
即:从小区间开始,不断向外扩展。*/

const int INF=2147483647;
int f[500][500];

int main() {
    string s; //输入序列s
    while(cin>>s){
        memset(f,0,sizeof(f));
	int n=s.size(); //计算序列长度
	for(int w=1;w<=n;w++) f[w][w]=1; //初始化
	for(int l=2;l<=n;l++) //区间长
	    for(int i=1;i<=n-l+1;++i){ //区间起点
	        int j=l+i-1; f[i][j]=INF;
	        if((s[i-1]=='('&&s[j-1]==')')||(s[i-1]=='['&&s[j-1]==']'))
	    	    f[i][j]=f[i+1][j-1]; //匹配成功,状态向外扩展
	       	for(int k=i;k<=j-1;k++) //枚举断点,将区间分成两个子问题
		    f[i][j]=min(f[i][j],f[i][k]+f[k+1][j]);         
	    }	 
	printf("%d\n",f[1][n]);
    }
    return 0;
}

 

【例题8】括号配对(升级版)

#include 
using namespace std;
typedef long long ll;

/*【括号配对】--输出此时的序列
定义如下规则序列:1.空序列是规则序列;
2.如果S是规则序列,那么(S)和[S]也是规则序列;
3.如果A和B都是规则序列,那么AB也是规则序列。
由‘(’,‘)’,‘[’,‘]’构成的序列,请添加尽量少的括号,得到一个规则序列。 */

/*【分析】枚举区间i,j,dp[i][j]表示添加的最少括号数,
如果i和j处的括号能够匹配,则dp[i][j]=dp[i+1][j-1]+1; 
即:从小区间开始,不断向外扩展。*/

//p.s.这是一个莫名其妙wa了的代码

string s; //注意:输入串可能是空串,不能用scanf
int f[500][500];

void print(int i,int j){ //递归法输出
    if(i>j) return;
    if(i==j){
        if(s[i]=='('||s[i]==')') printf("()");
        else printf("[]");
        return;
    }
    int ans=f[i][j]; //区间需要新加入的括号数
    if(((s[i]=='('&&s[j]==')')
        ||(s[i]=='['&&s[j]==']'))&&ans==f[i+1][j-1]){
        printf("%c",s[i]); print(i+1,j-1); 
        printf("%c",s[j]); return;
    }
    for(int k=i;k>s; memset(f,0,sizeof(f));
        int n=s.size(); //计算序列长度
        for(int i=0;i=0;i--) //逆序
            for(int j=i+1;j

洛谷AC版:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;
typedef long long ll;

/*【括号配对】--输出此时的序列
定义如下规则序列:1.空序列是规则序列;
2.如果S是规则序列,那么(S)和[S]也是规则序列;
3.如果A和B都是规则序列,那么AB也是规则序列。
由‘(’,‘)’,‘[’,‘]’构成的序列,请添加尽量少的括号,得到一个规则序列。 */

int stacks[101],top; //手写栈
char s[101],ss[101];

int main(){
    int n; scanf("%s",s);
    n=strlen(s);
    for(int i=0;i

 

【例题9】loj 10151 分离与合并

#include 
using namespace std;
typedef long long ll;

/*【loj 10151】分离与合体
分离完成的同时会有一面墙在 k 区域和 k+1 区域间升起,
阻断成 1..k 和 k+1..n 两个独立的区间,并在各自区间内任选除区间末尾之外,
(即从 1..k−1 和 k+1..n−1 中选取)的任意一个区域再次发生分离,
直到每个区间都只剩下一个区域,开始合体。所在区间中间的墙会消失。
合体会获得:(合并后所在区间左右端区域里价值之和)×(之前分离的时候所在区域的价值)
求可以获得的最大总价值,并按照分离阶段从前到后,区域从左到右的顺序,输出发生分离区域编号。 */

/*【分析】设区间[L,R]的最大值为f[L][R],用数组p[n]储存2^n的值,
采用记忆化搜索的办法,设k=m-(R-L),可以得到状态转移方程:
f[L][R]=max(num[L]*p[k]+dp(L+1,R),dp(L,R-1)+num[R]*p[k]),
最后输出的时候要{特判答案为0}的情况(第一个点)。*/

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int n,a[301];//n为区间数,a数组为区间里金钥匙的价值

int f[301][301],path[301][301];
//f[i][j]指的是从i~j区域可获得的金钥匙的最大价值
//path[i][j]存的是在i到j区域之间分裂的位置 

int q[301][2]; //q[i][0]为当前区间的左端点,q[i][1]为右端点 
    
void Print(){
    int l=1,r=1;
    q[1][0]=1,q[1][1]=n;
    while(l<=r){
        int a=path[q[l][0]][q[l][1]];
        cout<

 

【例题10】洛谷 p1005 矩阵取数

#include 
using namespace std;
typedef long long ll;
typedef __int128 bll;
const int MAXN=81;
using namespace std;

/*【矩阵取数】
给定的n*m的矩阵,矩阵中的每个元素aij均为非负整数。规则如下:
1.每次取数时须从{每行各取走一个元素},共n个;m次后取完矩阵所有元素;
2.每次取走的各个元素只能是该元素所在行的{行首或行尾};
3.一次取数得分为{每行取数的得分之和},每行取数的得分=被取走的元素值*(2^第i次);
4.游戏结束总得分为m次取数得分之和。求出取数后的最大得分。 */

/*【分析】设区间[L,R]的最大值为f[L][R],用数组p[n]储存2^n的值,
采用记忆化搜索的办法,设k=m-(R-L),可以得到状态转移方程:
f[L][R]=max(num[L]*p[k]+dp(L+1,R),dp(L,R-1)+num[R]*p[k]),
最后输出的时候要{特判答案为0}的情况(第一个点)。*/

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int n,m,num[MAXN];
bll ans,p[MAXN],f[MAXN][MAXN];

bll dp(int L,int R){
    if(f[L][R]!=-1) return f[L][R];
    if(R-L>=1) f[L][R]=max(num[L]*p[m-(R-L)]+dp(L+1,R),dp(L,R-1)+num[R]*p[m-(R-L)]);
    else f[L][R]=num[L]*p[m-(R-L)];
    return f[L][R];
}

void print(bll x){ //输出优化
    if(!x) return;
    if(x) print(x/10);
    putchar(x%10+'0');
}

int main(){
    reads(n); reads(m); p[0]=1;
    for(int i=1;i<=m;i++) p[i]=p[i-1]*2; //p数组记录2的n次方
    for(int i=1;i<=n;i++){
        for(int j=1;j<=m;j++) reads(num[j]);
        memset(f,-1,sizeof(f)); ans+=dp(1,m);
    }
    if(!ans) printf("0"); //判断特殊情况
    else print(ans);
    return 0;
}

 

【例题11】金字塔 请见 这里

 

{ 3. 环形结构上的DP问题 }

枚举选择一个位置把环断开,换成线性结构进行计算【破环成链】。

断开之后,把原链复制一遍。这样,*2的链上可以表示环中的任一区间。

代码实现:

for(int i=1;i<=N;i++)
    v[N+i]=v[i];
//注意:数组要开2*N大小。

 

【例题】洛谷 SP283 Naptime

  • 在某个星球上,一天由N小时构成。0-1点为第一个小时,1-2点为第二个小时...
  • 在第i个小时睡觉能恢复Ui点体力。在这座星球上住着一头牛,
  • 它每天要休息B个小时,它休息的这B个小时可以不连续,可以分成若干段,
  • 但是在每一段的第一个小时不能恢复体力,从第二个小时开始才可以恢复体力。
  • 为了身体健康,这头牛希望遵循生物钟,每天采用相同的睡觉计划。
  • 另外,因为时间是连续的,这头牛只需要保持在N个小时内休息B个小时就够了。
  • 请你给这头牛安排一个任务计划,使得它每天恢复的体力最多。

【分析】环形结构上的DP问题。

按线性完成第一重DP。在破环的位置执行第二次DP,综合答案求出最值。

【缄*默】 #DP# 各种DP的实现方法(更新ing)_第1张图片

缺少的情况就是(某一天)第一个小时在休息。

为了弥补缺少,可以强制令第N个小时和第二天的第一个小时都在休息。

这样就仍可以采用线性DP的方法,进行第二次DP

修改初值为:f[1][1][1]=U1,其余为负无穷;目标:f[N][B][1] 。


【代码实现】

#include 
using namespace std;
typedef long long ll;

/*【洛谷SP283】Naptime [环形DP]
在某个星球上,一天由N小时构成。0-1点为第一个小时,1-2点为第二个小时...
在第i个小时睡觉能恢复Ui点体力。在这座星球上住着一头牛,
它每天要休息B个小时,它休息的这B个小时可以不连续,可以分成若干段,
但是在每一段的第一个小时不能恢复体力,从第二个小时开始才可以恢复体力。
为了身体健康,这头牛希望遵循生物钟,每天采用相同的睡觉计划。
另外,因为时间是连续的,这头牛只需要保持在N个小时内休息B个小时就够了。
请你给这头牛安排一个任务计划,使得它每天恢复的体力最多。 */

const int MAXN = 4000;
int f[MAXN][MAXN][3],a[MAXN];
//f[i][j][1]表示(一天内)前i个小时休息了j个小时,并且第i个小时在休息,体力max
//f[i][j][0]表示(一天内)前i个小时休息了j个小时,并且第i个小时不休息,体力max

int main(){
    int n,m,T; scanf("%d",&T);
    while(T--){
        int ans=0; scanf("%d%d",&n,&m);
        for(int i=1;i<=n;++i) scanf("%d",&a[i]);
        f[1][0][0]=f[1][1][1]=0; //第一次DP开始,设置边界
        for(int i=2;i<=n;++i)
            for(int j=1;j<=min(i,m);++j){
                f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]);
                if(j!=1) f[i][j][1]=max(f[i-1][j-1][0],f[i-1][j-1][1]+a[i]); 
            }
        ans=max(f[n][m][1],f[n][m][0]); //先记录第一次DP得到的最值

        f[1][1][1]=a[1]; f[1][0][0]=0; //第二次DP开始,修改边界
        for(int i=2;i<=n;++i)
            for(int j=1;j<=min(i,m);++j){
                f[i][j][0]=max(f[i-1][j][0],f[i-1][j][1]);
                if(j!=1) f[i][j][1]=max(f[i-1][j-1][0],f[i-1][j-1][1]+a[i]); 
            }
        ans=max(ans,f[n][m][1]); //记录第二次DP得到的最值    
        printf("%d\n",ans);
    }
    return 0;
}

 

【例题】环路运输(环形DP+单调队列)

#include 
using namespace std;
typedef long long ll;

/*【CH5501】环路运输
在一条环形公路旁均匀地分布着N座仓库,编号为1~N,
编号为 i 与 j 的仓库之间的距离定义为 dist(i,j)=min⁡(|i-j|,N-|i-j|)。
也就是逆时针或顺时针从 i 到 j 中较近的一种。编号为 i 的仓库库存量为 A_i。
在 i 和 j 两座仓库之间运送货物需要的代价为 A_i+A_j+dist(i,j)。
求在哪两座仓库之间运送货物需要的代价最大。1≤N≤10^6,1<=Ai<=10^7。*/

const int mods=100003;

int a[5000019],anss;
deque q;

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int main(){
    int n; reads(n);
    for(int i=1;i<=n;i++){
        reads(a[i]); a[i+n]=a[i]; //破环为链
    }
    for(int i=1;i<=2*n;i++){ //使用单调队列进行维护
        while(!q.empty()&&i-q.front()>n/2) //队列中有合适的元素
            q.pop_front(); //队首元素出队
        if(i>=n) anss=max(anss,a[i]+i+a[q.front()]-q.front()); //统计答案
        while(!q.empty()&&a[q.back()]-q.back()

 

{ 4. 后效性的处理 }

 

【例题】Broken Robot (dp+高斯消元)

http://www.cnblogs.com/caturra/p/9459017.html

 

【例题】

https://blog.csdn.net/sinat_34550050/article/details/62052057

 

四. 树形DP

给定一棵有n个节点的树(通常是无根树,就是有n-1条无向边),

一般就以节点从深到浅(子树从小到大)的顺序作为DP的 “阶段” 。

DP状态表示中,第一维通常是节点编号(代表以该结点为根的子树)。

对于每个节点x,先递归它的每个子节点,在子节点上进行DP,

回溯时,从子节点向节点x进行状态转移。

递归子结构一般有两个方向:

  • 叶-->根,根的子节点传递有用的信息给根,由根得出最优解。
  • 根-->叶,取所有点作为一次根节点进行求值,此时父节点得到了整棵树的信息。
  • 只需要去除这个儿子DP值的影响,就能把状态传递给这个儿子。

 

{ 1.经典问题 }

1、HDU 1520 Anniversary party 子节点和父亲节点不能同时选,问最大价值。题解

2、HDU 2196 Computer 求树上每个点能到达的最远距离  题解

3、Choosing Capital for Treeland 给一个n节点的有向无环图,要找一个这样的点:

该点到其它n-1要逆转的道路最少,(边,如果v要到u去,则要逆转该边方向)题解

4、Godfather  求树的重心(删除该点后子树最大的最小)题解

 

5、POJ 3107 Tree Cutting  求删除哪点后子树最的小于等于节点总数的一半 题解

6、POJ 3140 Contestants Division 删除某边后剩余两个分支差值最小是多少 题解

7、HDU 3586 Information Disturbing 给定一个带权无向树,

要切断所有叶子节点和1号节点(总根)的联系,

每次切断边的费用不能超过上限limit,问在保证总费用<=m下的最小 limit。题解

8、POJ 3162 Walking Race  一棵n个节点的树。wc爱跑步,跑n天,

第i天从第i个节点开始跑步,每次跑到距第i个节点最远的那个节点(产生了n个距离),

现在要在这n个距离里取连续的若干天,使得这些天里最大距离和最小距离的差小于M,

问怎么取使得天数最多?题解

9、HDU 5834 Magic boy Bi Luo with his excited tree 每个节点有一个价值Vi,

每走一次一条边要花费Ci,问从各个节点出发最多能收获多少价值。 题解

10、POJ 2152 Fire n个节点组成的树,要在树一些点上建立消防站,每个点建站都有个cost[i],

每个点如果不在当前的点上建站,也要依赖其他的消防站,并且距离不超过limit[i]。

求符合上述条件的最小费用建站方案。 题解

11、POJ 1741 Tree 求树上距离小于等于K的点对有多少个。 题解

 

{ 2.相关知识点总结 }

1:给出一棵树 每个节点有权值  要求父节点和子节点不能同时取 求能够取得的最大值。 (hdu1520)

 

2:给出一棵树,求离每个节点最远的点的距离 (hdu2196)

 

3:1> 有N座城堡,每座城堡都有宝物,在每次游戏中允许攻克M个城堡并获得里面的宝物。

         有些城堡不能直接攻克,要攻克这些城堡必须先攻克其他某一个特定的城堡。

         求获得尽量多的宝物应该攻克哪M个城堡。 (hdu1561) 题解:树形dp+背包 (同选课)

    2> 每个节点有两个值bug和brain,当清扫该节点的所有bug时就得到brain值,

         只有当父节点被清空时,才可以清扫它的子节点,而清扫需要一定的人员。

         给定M个人员,N个结点的树,求最大brain和。 (hdu1011)

    3> 现在有n个村子,你想要用收买m个村子为你投票,其中收买第i个村子的代价是val[i]。

         但是有些村子存在从属关系,如果B从属于A国,则收买了A也意味着买通了B,

         而且这些关系是传递的。问你最小要付出的代价是多少? (poj3345)

 

4:1> 一棵树,定义每个节点的balance值:去掉这点节点后的森林里所有树的最大节点数。

         求出最小的balance值和其所对应的节点编号(poj1655)

     2> 无向树 T,依次去除树中的某个结点,求去掉该结点后变成的森林 T' 中的最大分支。

         并要求该分支为去除的结点尽可能少。按照节点编号从小到大输出。 (poj3107)

 

5:给一棵树, n结点<=1000, 和K< =200,  在这棵树上找大小为k的子树, 使其点权和值最大 (zoj3201)

 

6:给一个树状图,有n个点。求出:去掉哪个点,使得剩下的每个连通子图中点的数量不超过n/2。

     如果有很多这样的点,就按升序输出。n<=10000。 (poj2378)

 

7:n结点带权无根树,从中删去一条边,使得两棵子树的节点权值之和的绝对值最小,

     并求出得到的最小绝对值。 (poj3140)

 

8:给出一些点、边,然后求去掉一条边后将分成连通的两部分,且两部分的差值最小。 (hdu2242)

 

9:有n个点组成一个树,问至少要删除多少条边才能获得一棵有p个结点的子树。 (poj1947)

 

10:一棵树n<=1000(节点的分支<=8),Snail在根处,

      它要找到在某个叶子处的house而其中一些节点上有worm,

      worm会告诉它的house是否此子树上。求Snail最快寻找到house走过路径的期望值。 (poj2057)

 

11:给你一颗苹果树,有N个节点每个节点上都有一个苹果也就是有一个权值,

      当你经过这个节点是你将得到这个权值,重复走节点是只能算一次,

      给你N-1条边。问你只能走K步能得到的最大权值和。 (poj2486)

 

12:一颗二叉苹果树树上结苹果,要求剪掉几棵枝,然后求保留Q根树枝能保留的最多到苹果数 (ural1018)

 

13:给定一棵树,求最少连多少条边,使得每个点在且仅在某一个环内。 (poj1848)

 

14:在一棵树形的城市中建立一些消防站,但每个城市有一个最大距离限制,求需要的最小花费 (poj2152)

 

{ 3.例题总结 }

【例题1】洛谷 p1352 没有上司的舞会

#include 
using namespace std;

/*【洛谷p1352】
如果没有直接上司参加,就可以快乐,每个职员有一个快乐指数Hi。
请一部分职员参加,求参加职员的快乐指数总和max。 */

/*【分析】f[x][2]:第一维存节点编号,第二维表示状态转移。
f[x][0]表示x不参加,x的子树中开心值max。
则f[x][0]=(sum)max(f[son[x]][0],f[son[x]][1])。
f[x][1]表示x参加时子树开心值max。此时所有直接下属都不能参加。
则f[x][1]= H[x]+(sum)f[son[x]][0]。 */

vector son[10010]; //son[x][i]:节点x的根节点集合
int f[10010][2],v[10010],h[10010],n;

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int dp(int x){
    f[x][0]=0; f[x][1]=h[x]; //初始化
    for(int i=0;i

 

【例题2】洛谷 p2014 选课(背包类树形DP)

  • 现在有N门功课,每门课有学分,每门课有一门或没有直接先修课。
  • 某人要从这些课程里选择M门课程学习,问他能获得的最大学分是多少?

【分析】每门课最多只有一门先修课,对应树中每个节点最多只有一个父亲。

所以这n门课程构成了森林结构。新建一个“虚拟课程”为0号节点。

0号节点作为“实际上没有先修课的课程”的先修课。把森林转化成树。

f[x][t]表示以x为根的子树中、选t门课能够获得的最高得分。

<背包类树形DP> 又称有树形依赖的背包问题。

除了以 “节点编号” 作为树形DP的阶段(第一维度),

还要把当前背包的 “体积” 作为第二维状态。


【代码】

#include 
using namespace std;
typedef long long ll;

/*【洛谷p2014】选课(背包类树形DP)
现在有N门功课,每门课有学分,每门课有一门或没有直接先修课。
某人要从这些课程里选择M门课程学习,问他能获得的最大学分是多少? */

/*【分析】每门课最多只有一门先修课,对应树中每个节点最多只有一个父亲。
所以这n门课程构成了森林结构。新建一个“虚拟课程”为0号节点。
0号节点作为“实际上没有先修课的课程”的先修课。把森林转化成树。
f[x][t]表示以x为根的子树中、选t门课能够获得的最高得分。 */

vector son[310];
int f[310][310],s[310],n,m;

void dp(int x){
    f[x][0]=0;
    for(int i=0;i=0;t--) //倒序循环当前选课总门数(当前背包体积)
            for(int j=t;j>=0;j--) //循环更深子树上的选课门数(组内物品)
                if(t-j>=0) f[x][t]=max(f[x][t],f[x][t-j]+f[y][j]); //选择一课新子树
    }
    if(x!=0) //x不为0时,选修x本身需要占用1门课,并获得相应学分
        for(int t=m;t>0;t--) f[x][t]=f[x][t-1]+s[x]; //必须要选x
}

int main(){
    cin>>n>>m; //n门课程中选出m门
    for(int i=1;i<=n;i++){
        int x; cin>>x>>s[i]; //先修课和学分
        son[x].push_back(i); //划入先修课的直接孩子中
    }
    memset(f,0xcf,sizeof(f)); //-INF
    dp(0); cout<

 【优化】

可以用 “左儿子右兄弟” 的办法把多叉树转为二叉树。

这样,状态转移的时候就只需要枚举左右子树。

#include 
using namespace std;
typedef long long ll;

/*【洛谷p2014】选课(背包类树形DP)
现在有N门功课,每门课有学分,每门课有一门或没有直接先修课。
某人要从这些课程里选择M门课程学习,问他能获得的最大学分是多少? */

/*【分析】每门课最多只有一门先修课,对应树中每个节点最多只有一个父亲。
所以这n门课程构成了森林结构。新建一个“虚拟课程”为0号节点。
0号节点作为“实际上没有先修课的课程”的先修课。把森林转化成树。
f[x][t]表示以x为根的子树中、选t门课能够获得的最高得分。 */

struct node{
    int lc,rc,c,v; //c表示对应值,v表示子树的最后一个子节点
    node(){ lc=rc=-1; v=0; } //初始化
}tr[110000];

int f[1100][1100]; //f[i][j]表示i的子树中、选择j个子树节点得到的最大学分
 
int treedp(int x,int y){ //以x为根的子树保留y个节点,求值f[x][y+1]

    if( x<0 || y<0 )  return 0; //节点不存在
    if(f[x][y]!=-1)  return f[x][y]; //如果已经讨论过这种情况,退出
    int maxx=0;
    for(int i=0;i<=y;i++){ //分配到右子树中的课程数i
        int ls=y-1-i,rs=i,lss=0,rss=0; //左右子树管理个数和最大得分
        if(tr[x].lc!=-1) //左边最大得分
            lss=f[tr[x].lc][ls]=treedp(tr[x].lc,ls);
        if(tr[x].rc!=-1) //右边最大得分
            rss=f[tr[x].rc][rs]=treedp(tr[x].rc,rs);
        if(ls<0) maxx=max(maxx,rss); //如果只有右边
        else maxx=max(lss+rss+tr[x].c,maxx);
    }
    return maxx;
}

int main(){
    int n,m; scanf("%d%d",&n,&m); //n个点,要留m个
    for(int i=1;i<=n;i++){
        int xx; //xx作为i的先修课
        scanf("%d%d",&xx,&tr[i].c); //tr[i].c是该课程学分
        //↓↓↓【左孩子右兄弟】法
        if(tr[xx].v==0) tr[xx].lc=i; //如果现在没孩子,做左孩子
        else tr[tr[xx].v].rc=i; //已有孩子,去最深的子节点做右孩子
        tr[xx].v=i; //记录已更新的右孩子最后节点(链式记录)
    }
    memset(f,-1,sizeof(f));
    for(int i=0;i<=n;i++)  f[i][0]=0; //初始化,一门课都不选
    printf("%d\n",treedp(0,m+1)); //
    return 0;
}

 

【例题3】洛谷 p2015 二叉苹果树

#include 
using namespace std;
typedef long long ll;

/*【洛谷p2015】二叉苹果树
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)。
这棵树共有N个结点(叶子点或者树枝分叉点),编号为1~~N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。 */

struct node{ //建边,链式前向星存储
    int x,y,d,next;
}a[210];int len=0,last[110];

void ins(int x,int y,int d){
    len++;//该节点编号增加
    a[len].x=x;  a[len].y=y;  a[len].d=d;
    a[len].next=last[x];   last[x]=len;
}
     
struct trnode{ //建立树的管理节点的左右儿子
    int l,r;
    trnode(){ l=0;r=0; }
}tr[110];

int f[110][110];//f[i][j]表示第i个节点的子树保留j个点得到的最大值
int n,k;
bool vis[110];//判断是否访问过

void dfs(int x){ //从根节点向下dfs建树
    for(int k=last[x];k;k=a[k].next){ //跳转到x关联的下一个边
        int y=a[k].y;
        if(vis[y]==true){ //没访问过
            vis[y]=false;
            f[y][1]=a[k].d; //初始化y结点:只保留一个k点的情况
            if(tr[x].l==0) tr[x].l=y; //如果还没有左儿子,就是左儿子
            else tr[x].r=y; //否则是右儿子
            dfs(y); //访问y相连的边
        }
    }
}

int treedp(int x,int kk){ //以x为根的子树保留kk个节点,求值f[x][kk+1]
    if(x==0) return 0; //不该存在0节点
    if(f[x][kk]!=-1) return f[x][kk]; //讨论过f[x][kk],直接返回
    int maxx=0;
    for(int i=0;i<=kk-1;i++){ //选择要舍弃的,分给左边i个
        int ls,rs;
        ls=i; rs=kk-1-i; //左边留i个,中间的根节点必须选择留下,右边留kk-1-i个
        int tpl=treedp(tr[x].l,ls); //左边最大利益
        int tpr=treedp(tr[x].r,rs); //右边最大利益
        maxx=max((tpl+tpr+f[x][1]),maxx);
    }
    f[x][kk]=maxx; //存储这个此时到达的子树的最大值
    return maxx; //每次传上这个更新后的maxx,便于下一步剪枝
}

int main(){
    scanf("%d%d",&n,&k);//n个点,要留k个
    len=0;  memset(last,0,sizeof(last));
    int x,y,c;
    for(int i=1;i

 

【例题4】洛谷 p2016 战略游戏

#include
#include
#include
#include
#include
#include
#include
using namespace std;
typedef long long LL;

//【树的最大独立集问题】

/*【洛谷p2016】战略游戏【树形DP】
建立一个古城堡,城堡中的路形成一棵树,他要在这棵树的结点上放置最少数目的士兵。
与该结点相连的所有{边}将都可以被望到,使得这些士兵能望到所有的路(覆盖所有边)。
计算出他需要放置最少的士兵个数。   */

//注意:这题和【皇宫看守】的不同之处在于,这里是【边】,那一题是【点】

LL v[11000];
LL f[11000][3];
/*【分析】只有两种情况,因为每边必须安全。
f[x][0]此点不放人,但连向y的边安全;
f[x][1]此点放人,但连向y的边安全。*/

struct node{
    LL x,y,next;
}a[110000];
LL last[110000],len;

void ins(LL x,LL y){ //链式前向星建边
    len++; a[len].x=x; a[len].y=y; 
    a[len].next=last[x]; last[x]=len;
}

bool vis[11000];
void treedp(int x){
    f[x][1]=v[x];  f[x][0]=0; //放与不放初始值不同
    for(int k=last[x];k;k=a[k].next){
        LL y=a[k].y; //寻找x相连的边
        if(vis[y]==false){ //还没有访问过
            vis[y]=true; //标记
            treedp(y); //递归求值【注意:先递归再判断f的更新】
            f[x][0]+=f[y][1]; //x不放人,y必须放人(边只连两点)
            f[x][1]+=min(f[y][1],f[y][0]); //y安全的最小值
        }
    }
}

int main(){
    LL n; while(scanf("%lld",&n)!=EOF){
        memset(f,0,sizeof(f));
        for(int i=1;i<=n;i++){
            LL x,m; //x是节点编号,m表示连向边的个数
            scanf("%lld%lld",&x,&m);
            v[x]=1; //每点价值都是1
            for(int j=1;j<=m;j++){
                LL y; scanf("%lld",&y);
                ins(x,y); ins(y,x); //直接建双向边
            }
        }
        LL root=1; //设置根节点
        memset(vis,false,sizeof(vis)); vis[root]=true;
        treedp(root); //从节点开始搜索
        printf("%lld\n",min(f[root][0],f[root][1]));
    }
    return 0;
}

 

【例题5】洛谷 p1040 / noip 2003 加分二叉树

#include
#include
#include
#include
#include
#include
#include
#include
#include
using namespace std;

/*【加分二叉树】noip2013/洛谷1040
设一个n个节点的二叉树tree的中序遍历为(节点编号1,2,3,…,n)。
每个节点都有一个分数(均为正整数),记第i个节点的分数为di,
tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数。
若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空子树。
试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出:
(1)tree的最高加分(2)tree的前序遍历 。 */

/*【分析】树形DP。
1.dp计算最大分值===设f[i,j]为顶点i~j所组成的子树的最大分值。
若f[i,j] = -1,则表明最大分值尚未计算出。枚举i~j之间的k值:
f(i,j)={1(i>j);顶点i的分数(i=j);max(f{i,k-1}*f{k+1,j}+顶点i的分数(ir) return 1;
    if(f[l][r]==-1) //未计算过
        for(int k=l; k<=r; k++) { //【枚举子根k】
            now=search(l,k-1)*search(k+1,r)+f[k][k];  
            if(now>f[l][r]){ //更新并记下子根
                f[l][r]=now;  root[l][r]=k;
            }
        }
    return f[l][r];
}

void predfs(int l,int r){ //前序遍历顶点l~r对应的子树
    if(l>r) return; //递归边界
    if(firstone) firstone=false; //输出的第一位前方没有空格
    else cout<<' '; //顶点间用空格分开
    cout<

 

【例题6】caioj 1111 皇宫看守

#include
#include
#include
#include
#include
#include
#include
using namespace std;

/*【树形DP】 皇宫看守
皇宫以午门为起点,呈一棵树的形状;有边直接相连的宫殿可以互相望见
每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同
没法在每个宫殿都安置留守侍卫,求在看守全部宫殿的前提下,最少花费的经费。 */

typedef long long LL;
struct node{
    LL x,y,next;
}a[110000];
LL last[110000],len;

void ins(LL x,LL y)//日常建边
{
    len++;//链式前向星排入
    a[len].x=x; a[len].y=y; 
    a[len].next=last[x]; last[x]=len;
}

LL v[11000];
LL f[11000][5];/*4种状态【分析状态最重要】
f[x][0]此点不放人,但安全
f[x][1]此点不放人,不安全
f[x][2]此点放人,并安全
f[x][3]此点放人,不安全(不存在)
》》此处安全指以x为根的子树都安全
》》不安全指以x为根的子树除x都安全 */

bool bk[11000];
void treedp(int x)
{
    f[x][2]=v[x];f[x][1]=0;f[x][0]=0;//放与不放初始值不同
    LL minn=999999999;
    bool bkk=false;
    for(int k=last[x];k;k=a[k].next)
    {
        LL y=a[k].y;//相连的边的连向,则y是x的儿子
        if(bk[y]==false)//还没有访问过
        {
            bk[y]=true;//标记
            treedp(y);//递归求值
            minn=min(f[y][2]-f[y][0],minn);
            //(??)存f[x][2]和f[x][0]差值最小值,方便最后增加
            f[x][0]+=min(f[y][0],f[y][2]);//加上y节点最小值(y安全的情况下)
            if(f[y][2]<=f[y][0]) bkk=true;
            //f[y][2]<=f[y][0]时,选f[y][2](站人且花费小)
            f[x][1]+=f[y][0];//x不安全,则y只能不站人(必须安全)
            f[x][2]+=min(f[y][2],min(f[y][1],f[y][0]));//x放人的最大值
            //此时y会被f[x][2]影响,要继承f[y][1]
        }
    }
    if(bkk==false) f[x][0]+=minn;
    //如果没有任何y,站人花费小于不站人,只能把最小相差加上
    //使得至少一个y满足f[y][2](有一个点放人)
}

int main()
{
    LL n; scanf("%lld",&n);
    memset(f,0,sizeof(f));
    for(int i=1;i<=n;i++) 
    {
        LL x,m,k;
        scanf("%lld%lld%lld",&x,&k,&m);
        v[x]=k;
        for(int j=1;j<=m;j++)
        {
            LL y; scanf("%lld",&y);
            ins(x,y); ins(y,x);//建立双向边
        }
    }
    LL root=1;//设置根节点
    memset(bk,false,sizeof(bk)); bk[root]=true;
    treedp(root);
    printf("%lld\n",min(f[root][0],f[root][2]));//一定要完全安全
    return 0;
}

 

【例题7】数字转换 这里

【例题8】旅游规划 这里

【例题9】骑士 这里

【例题10】叶子的颜色 这里

 

{ 4.二次扫描与换根法 }

【例题】poj 3585

 

五. 图论DP

 

六. 数位DP+状压DP

{ 一. 数位DP }

一般问题:求出在给定区间 [ A , B ] 内,符合条件 P ( i ) 的数 i 的个数。

条件 P ( i ) 一般与数的大小无关,而与 数的组成 有关。

即:给定一些限制条件,求满足限制条件的第K小的数是多少。

利用数位的性质,设计log级别复杂度的算法。

最基本的思想是”逐位确定“,其预处理的过程也可以看做数位DP。

狂戳 这里 qwq

【例题1】Amount of Degree

所求的数为互不相等的幂之和,即起B进制表示的各位数字都只能是0和1,

所以我们只考虑二进制的情况,且其他进制也可以转化为二进制。

本题满足区间减法,即 count [ i...j ] = count [ j ] - count [ i-1 ]

对于n,我们只需求0-n有多少个满足条件的。

只需要考虑上界的限制,这是数位DP中最常用的技巧。

【缄*默】 #DP# 各种DP的实现方法(更新ing)_第2张图片

#include 
using namespace std;
typedef long long ll;

/*【ural1057】Amount Of Degrees
求给定区间[X,Y]中满足下列条件的整数个数:
这个数恰好等于K个互不相等的B的整数次幂之和。*/

int x,y,b,len,pos[35];
int f[35][35]; 
//设f[i][j]表示深度(从下往上编号)为i的完全二叉树的、
//二进制表示中,恰好含有j个1的数的个数。

void init(){ //预处理
    memset(f,0,sizeof(f));
    f[0][0]=1; //设置起点
    for(int i=1;i<=32;i++){
        f[i][0]=f[i-1][0];
        for(int j=1;j<=i;j++)
            f[i][j]=f[i-1][j-1]+f[i-1][j];
    }
}

int solve(int t, int k){ //统计[0...x]区间内二进制表示下含k个1数的数字个数
    int ans=0,tot=0;
    for(int i=31;i>0;i--){
        if(x&(1<k) break;
            x=x^(1<

 

【例题2】poj 3208 启示录

#include 
using namespace std;
typedef long long ll;

/*【poj3208】启示录
只要某数字的十进制表示中有三个连续的6,就认为这个是魔鬼的数,
比如666, 1666, 2666, 3666, 6663, 6660666等等。
请输出“第X大的魔鬼的数”。*/

/*【分析】
设f[i,3]表示由i位数字构成的魔鬼数的个数。
设f[i,j]表示由i位数字构成的、开头连续有j个6的非魔鬼数的个数。
计算f时,允许前导0的存在。状态转移方程:
f[i][0]=9*(f[i-1][0]+f[i-1][1]+f[i-1][2]); //前导不是‘6’
f[i][1]=f[i-1][0]; f[i][2]=f[i-1][1];
f[i][3]=f[i-1][2]+10*f[i-1][3]; //前导‘66’或者之前就满足条件 */

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

long long f[21][4]; //有20位数字组成就已经足够了


void prework() {
    f[0][0]=1;
    for(int i=0;i<20;i++) {
        for (int j=0; j < 3; j++) {
            f[i + 1][j + 1] += f[i][j];
            f[i + 1][0] += f[i][j] * 9;
        }
        f[i + 1][3] += f[i][3] * 10;
    }
}

int main() {
    prework();
    int t,n,m; reads(t);
    while(t--){
        reads(n); //题目中的X,第n小的魔鬼数
        for(m=3;f[m][3]

 

【例题3】xdu 1160 不降数

#include 
using namespace std;
typedef long long ll;

/*【xdu1160】不降数
不降数必须满足从左到右【各位数字成大于等于的关系】如123,446。
指定一个整数闭区间[a,b],问这个区间内有多少个不降数。*/

/*【分析】此问题满足区间可减性,区间可以用上界之差表示出来。
从高位到低位枚举,dp[pos][statu]表示在pos数位,数字是statu(0~9)的方案数。
dfs(pos,statu,done),枚举pos的statu,更新答案。 */

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int dp[12][12],a,b;
vector digit;

int dfs(int pos,int statu,int done){ //pos不断减小(从高位到低位进行DP)
    if(pos==-1) return 1;
    if(!done && ~dp[pos][statu]) return dp[pos][statu];
    int res=0; int ends=done? digit[pos]:9;
    for(int i=statu;i<=ends;i++){ //在下一位(较低位)填一个更大的数字
        res+=dfs(pos-1,i,done&&i==ends);
    }
    if(!done) dp[pos][statu]=res;
    return res;
}

ll solve(int num){ //上界
    memset(dp,-1,sizeof(dp));
    digit.clear();
    while(num>0){
        digit.push_back(num%10);
        num /= 10;
    }
    return dfs(digit.size()-1,0,1);
}

int main(){
    while(cin>>a>>b) cout<

 

【例题4】xdu 1161 各位数字和

#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;

/*【xdu1161】数字游戏之各位数字和
某人命名了一种取模数,这种数字必须满足各位数字之和 mod N为0。
指定一个整数闭区间[a,b],问这个区间内有多少个取模数。 */

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int f[100][111],bit[100],a,b,n;

int dfs(int pos,int res,bool limit){
    if(pos==-1) return res==0;
    if(!limit&&~f[pos][res]) return f[pos][res];
    int end=limit?bit[pos]:9,ans=0;
    for(int i=0;i<=end;i++){
        int newres=(res+i)%n;
        ans+=dfs(pos-1,newres,limit&&(i==end));
    }
    if(!limit) f[pos][res]=ans;
    return ans;
}

int counts(int x){ //计算上界之内的取模数个数
    int len=0;
    while(x){ 
        bit[len++]=x%10;
        x/=10;
    }
    return dfs(len-1,0,true);
}

int main(){
    while(scanf("%d%d%d",&a,&b,&n)!=EOF){
        memset(f,-1,sizeof(f));
        printf("%d\n",counts(b)-counts(a-1));
    }
    return 0;
}

 

【例题5】洛谷 p2657 Windy数

#include
#include
#include
#include
#include
using namespace std;
typedef long long ll;

/*【p2627】Windy数【两次数位DP】
不含前导零且[相邻两个数字之差至少为2]的正整数被称为windy数。
在区间[a,b]之间,总共有多少个windy数? */

/*【分析】设f[i][j]为长度为i、且最高位是j的windy数的个数。
转移方程:f[i][j]=sum(f[i-1][k]); 其中 abs(j-k)>=2。  */

void reads(int &x){ //读入优化(正负整数)
    int f=1;x=0;char s=getchar();
    while(s<'0'||s>'9'){if(s=='-')f=-1;s=getchar();}
    while(s>='0'&&s<='9'){x=x*10+s-'0';s=getchar();}
    x*=f; //正负号
}

int f[15][15],a[15];

void init(){
    for(int i=0;i<=9;i++) f[1][i]=1; //0,1,2,...9都属于windy数 
    for(int i=2;i<=10;i++)
        for(int j=0;j<=9;j++)
            for(int k=0;k<=9;k++)
                if(abs(j-k)>=2) f[i][j]+=f[i-1][k]; 
    //从第二位开始每次枚举最高位j,并找到k、使得j-k>=2。
}

int counts(int x){ //计算<=x的windy数 
    memset(a,0,sizeof(a));
    int len=0,ans=0;
    while(x){ a[++len]=x%10; x/=10; }

    for(int i=1;i<=len-1;i++)//先求len-1位的windy数,必定包含在区间里的
        for(int j=1;j<=9;j++) ans+=f[i][j];
    
    for(int i=1;i=1;i--){ //i从最高位后开始枚举 
        for(int j=0;j<=a[i]-1;j++) //j是i位上的数 
            if(abs(j-a[i+1])>=2) //判断和上一位(i+1)相差2以上    
                ans+=f[i][j]; //如果是ans就累加 
        if(abs(a[i+1]-a[i])<2) break;
    }

    return ans;
}

int main(){
    int a,b; reads(a); reads(b); init();
    cout<

 

{ 二. 状压DP }

超级详细的状压DP讲解和例╰(*°▽°*)╯

 

七. 倍增优化DP

 

八. 数据结构优化DP

 

九. 单调队列优化DP

 

十. 斜率优化

----------斜率优化DP知识点总结----------

 

十一. 四边形不等式

 

 

 

 

 

 

p.s.缄默==专题知识点总结(大板块)

                                               ——时间划过风的轨迹,那个少年,还在等你。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

你可能感兴趣的:(C++,知识点,DP,专题)