目录
一. 线性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
解决一些线性区间上的最优化问题,会用到DP的思想,这种问题可以叫做线性DP。
DP的阶段沿各个维度线性增长,从边界点开始,有方向地向状态空间转移、扩展。
若“决策集合中的元素只增多不减少”,可以维护一个变量记录决策集合信息,避免重复扫描。
需要考虑:一个后续阶段的未知状态,需要哪些前阶段的已知状态去更新。
经典模型:最长上升子序列(LIS)、最长公共子序列(LCS)、最大子序列和、数字三角形等。
那么首先我们从这几个经典的问题出发开始对线性DP的探索。
#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]
#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;
}
#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;
}
#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
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;
}
其他优化请戳 这里 ...
#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;
}
【分析】首先要证明ans最小化的情况下,一定存在一种构造方式,
使得b数组在a数组中全部出现过。证明方法简述:
记原来的数从小到大排序后分别是a1 a2 a3...an,修改后分别是b1 b2 b3...bn。
为了方便描述,在数轴上标出这些点,称为关键点。
综上至少存在一种最优方案,最终的所有数都是原来的某个数。
最优解与两个参数有关:当前位置处理后的最大值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;
}
#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
背包问题的经典讲解可以参见背包详解,还有背包六问。
问题(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 ] } ;
//【背包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]);
//【背包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]);
转换成一维方程,需要 逆序 。
逆序的目的是:保证后一个 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]);
#include
#include
#include
#include
#include
#include
#include
问题(完全背包): 有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】把正整数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)<
问题(多重背包):
有n种重量和价值分别为vi和ci的物品,每种物品有si个。
从这些物品中挑出总重量不超过m的物品,求所有挑选方案中价值总和的最大值。
多重背包的特点:多维度,有限制存在。
转换成方程:f [ i ][ j ] = max{ f [ i - 1 ][ j - k*v[i] ] + k*w[i] | 0 <= k <= s[i] };
//【背包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]);
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;
}
【关键词】多重背包+可行性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,只有两类可能情况:
贪心策略: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
问题(分组背包):
有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]);
三. 区间DP
以 “ 区间长度 ” 作为DP的 “ 阶段 ”,用 “ 区间左右端点 ” 描述 “ 维度 ” 。
一个状态、由若干个比它更小、且包含于它的区间、所代表的状态转移而来。
区间DP的初态一般由长度为1的 “ 元区间 ” 构成(dp[i][i] 初始化为自身的值)。
特征:能将问题分解为两两合并的形式。也可以将多个问题整合分析。
典型应用:石子合并,能量项链,凸多边形的划分等问题。
区间DP的模式可以概括为:向下划分,再向上递推。
决策:dp[i][j]=min{ dp[i][k]+dp[k+1][j] | i<=k
区间DP的状态转移方法:
for(int len=1;len<=N;++len) //区间长度
for(int l=1,r=len;r<=N;++l,++r)
{ 考虑F[l][r]的转移方式 }
题目分析:
最大化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;
}
【分析】
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
【分析】
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
#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
#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
#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;
}
#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;
}
#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
#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<
#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】金字塔 请见 这里
枚举选择一个位置把环断开,换成线性结构进行计算【破环成链】。
断开之后,把原链复制一遍。这样,*2的链上可以表示环中的任一区间。
代码实现:
for(int i=1;i<=N;i++)
v[N+i]=v[i];
//注意:数组要开2*N大小。
【分析】环形结构上的DP问题。
按线性完成第一重DP。在破环的位置执行第二次DP,综合答案求出最值。
缺少的情况就是(某一天)第一个小时在休息。
为了弥补缺少,可以强制令第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;
}
#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()
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进行状态转移。
递归子结构一般有两个方向:
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的点对有多少个。 题解
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)
#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
【分析】每门课最多只有一门先修课,对应树中每个节点最多只有一个父亲。
所以这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;
}
#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
#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;
}
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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】叶子的颜色 这里
【例题】poj 3585
五. 图论DP
六. 数位DP+状压DP
一般问题:求出在给定区间 [ A , B ] 内,符合条件 P ( i ) 的数 i 的个数。
条件 P ( i ) 一般与数的大小无关,而与 数的组成 有关。
即:给定一些限制条件,求满足限制条件的第K小的数是多少。
利用数位的性质,设计log级别复杂度的算法。
最基本的思想是”逐位确定“,其预处理的过程也可以看做数位DP。
狂戳 这里 qwq
所求的数为互不相等的幂之和,即起B进制表示的各位数字都只能是0和1,
所以我们只考虑二进制的情况,且其他进制也可以转化为二进制。
本题满足区间减法,即 count [ i...j ] = count [ j ] - count [ i-1 ] 。
对于n,我们只需求0-n有多少个满足条件的。
只需要考虑上界的限制,这是数位DP中最常用的技巧。
#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<
#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]
#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<
#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;
}
#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知识点总结----------
十一. 四边形不等式
p.s.缄默==专题知识点总结(大板块)
——时间划过风的轨迹,那个少年,还在等你。