目录
鸣人的影分身(线性DP)
DFS解法
DP(完全背包)
思维解法递归解法
包子凑数(完全背包+数论)
糖果(01背包问题)
密码脱落(区间DP+数学)
括号配对(区间DP)
生命之树(树形DP)
旅游规划(树形DP)
前景提要:
(1)dp数组的含义:这个dp数组代表的意义是什么,[i][j]又分别代表什么意思
(2)dp数组的属性:包括最大值,最小值,方案数,次数,即dp数组本身存的数
(3)dp数组的初始化:根据它的含义进行合理的初始化
(4)dp数组的遍历顺序:是正序从前往后,还是后续遍历防止被重复覆盖
(5)dp数组的递推公式:从最后一步反推,即要得到这一步,需要上一步进行的操作
(6)dp数组的返回值:即题目要求的是dp数组的什么,要根据含义来
1050. 鸣人的影分身 - AcWing题库
思路解析:
透过题目看本质,这道题的本质是:
给出一个和,给出一个份数,求出“和”可以被分成多少个份数大小的集合
举个例子:总和为:7 份数为:3
那么就有:(注意顺序不同,集合内的数字相同被视为一种情况)
(7,0,0)
( 6,1,0)
(5,2,0) (5,1,1)
(4,3,0) (4,2,1)
(3,3,1) (3,2,2)
相信看完例子解释的你,已经想到了DFS,来暴力搜索
DFS代码如下:
DFS的核心之一就是参数的意义
(1)首先要输入总能量,和分身的数量
那么在向下搜索的过程中:
(2)思考深搜过程:
<1>需要一个标志变量,来标记当前从这个标志变量开始枚举,也就是变量start
这个就类似于(1~5)个数中(集合大小为3个数)的组合
<2>边界条件,当给被赋予能量的分身数量==给出的分身数量 且剩余能量为0
<3>剪枝,当枚举过程中,被赋予能量的分身数量大于给定的分身数量
#include
using namespace std;
int m, n;
int res;
// t:剩余的能量 start:下一个分身的值 state:已经完成的分身
void dfs(int t, int start, int state)
{
if (state == n && t == 0)//已经完成的分身数量==n 且剩余的能量为0 即返回
{
res++;//记录数量
}
//可不写“==”
if (state >= n) return;//已经完成的分身数量>=n,也返回(就算还有剩余能量)
for (int i = start; i <= t; i++)
{
dfs(t - i, i, state + 1);//剩余的能量-i,下一个从i开始枚举,分身的数量++
}
}
int main()
{
int T;
cin >> T;
while (T--)
{
cin >> m >> n;
res = 0;
dfs(m, 0, 0);//初始剩余的能量为:m
cout << res << endl;
}
return 0;
}
有没有一种可能这个是完全背包,即每个数可以被放入背包无限次
(1)dp数组的含义:总和为i,划分为j个数的所有情况
(2)dp数组的属性:符合题目条件的次数
(3)dp数组的递推公式:观察上面给出的例子可知:
对于最后一步有:划分为j个数情况:
<1>当i
划分为j个数的情况==划分j-1个数的情况+最后一列放0的情况
<2>当i>=j时,有两种情况:
当最小的数为不为0的情况,那么方案数==总和i-j个1的情况+最小的数为0的情况
·······1:有影子被分到的最小能量为0(最小的数为0的情况被<1>处理了)
·······2:影子的被分到的最小能量不为0
(4)dp数组的初始化:由递推公式可知:dp[i][0]的情况都是1
#include
#include
using namespace std;
const int N = 11;
int main()
{
int T;
scanf("%d", &T);
while (T -- )
{
int n, m;
scanf("%d%d", &m, &n);
int f[N][N] = {0};//dp数组的含义:总和为i,划分为j个数的所有情况
f[0][0] = 1;//当总数为0,划分为0个,情况数为:1
/*问题:
for(int i=0;i= j) f[i][j] += f[i - j][j];//当最小的数为不为0的情况,那么方案数==总和i-j个1的情况+最小的数为0的情况
}
printf("%d\n", f[m][n]);
}
return 0;
}
转换为:n个苹果放m个盘的经典问题
#include
#include
#include
using namespace std;
int f(int x, int y) {
if (x == 0) return 1;//没有苹果,全部盘子为0
if (y == 0) return 0;//没有盘子,没法放
if (y > x) {//盘子数大于苹果数,至多只能x个盘子上都放一个
return f(x, x);
}
return f(x - y, y) + f(x, y - 1);//盘子数小于等于苹果数 -> 分类讨论: 有盘子为空,没有盘子为空
//有盘子为空的时候即至少有一个盘子为空,f(x,y-1);没有盘子为空即最少每个盘子都有一个,f(x-y,y)
}
int main() {
int t, n, m;//n个苹果分到m个盘子里去,运行盘子为空
cin >> t;
while (t--) {
cin >> n >> m;
cout << f(n, m) << endl;
}
return 0;
}
活动 - AcWing
本题与:活动 - AcWing(买不到的糖果有异曲同工之妙)
复习一下:
给定a,b,若 d=gcd(a,b)>1 ,则一定不能凑出最大数
如果 a,b均是正整数且互质,那么由 ax+by,x≥0,y≥0不能凑出的最大数是 (a−1)(b−1)−1
在买不到的糖果那道题中,是给定两个数,求出这两个数不能组成的最大的数
而在本题中,给定的是任意N个数,求的是这N个数,不能组成的数的个数
DP分析:
(1)dp数组的含义:从前i项物品任意个,总和为j
(2)dp数组的属性:能够达到说明非空,不能达到说明为空
(3)dp数组的递推公式:
由于是完全背包,所以对于一个物品来说,可以选择多次,假设选择这个物品k次:
那么就将j分成了两部分:原本的部分(减去k件该物品的部分)+k件该物品的部分
那么只要知道那原本的部分是否可以满足,如果可以被满足,那么就为true,所以是用的或运算符|,只要其中有一个部分可以满足,那么再挑选这个部分那么必然也是满足的
(4)dp数组的初始化:
由递推公式可知:dp[0][0]应被初始化为true,因为从0个物品中总和为0,是可以满足的
那么对于不选择该物品的情况,就应该让它等于上一个选择物品的情况
(5)dp数组的遍历顺序:
正序两层for循环即可
(6)dp数组的返回值:
统计不满足的个数即可
#include
#include
using namespace std;
const int N = 10010;//历史遗留问题:根据数据范围猜的
int a[110];
bool f[110][N];
int gcd(int a, int b)
{
return b ? gcd(b, a % b) : a;
}
int main()
{
int n;
scanf("%d", &n);
int d = 0;
for (int i = 1; i <= n; i++)
{
scanf("%d", &a[i]);
d = gcd(d, a[i]);
}
if (d != 1) puts("INF");
else
{
f[0][0] = true;
for (int i = 1; i <= n; i++)
for (int j = 0; j < N; j++)
{
f[i][j] = f[i - 1][j];
if (j >= a[i]) f[i][j] |= f[i][j - a[i]];
}
int res = 0;
for (int i = 0; i < N; i++)
if (!f[n][i])
res++;
printf("%d\n", res);
}
return 0;
}
1047. 糖果 - AcWing题库
思路解析:
经典的01背包问题:即每个糖果只能选择一次
(1)dp数组的含义:所有从前i个物品中选,且总和除以k为j的糖果数量
(2)dp数组的属性:糖果的数量
(3)dp数组的递推公式:对于最后一步有:选和不选两种情况,两者取最大即可
<1>不选择该包糖果,那么此时的糖果数量还是等于上一个的数量
<2>选择该包糖果,那么此时糖果的数量等于上一个的糖果数量+该包糖果的数量
注意:对于选择该包糖果的情况,需要对余数j进行正数处理
(4)dp数组的初始化:y总说,对于没有意义的,要么初始化为负无穷要么初始化为正无穷,那么这里由于是要求最多的糖果数量,那么一定是要初始化为负无穷的
那么对于有意义的值:dp[0][0],也就是前0个物品中,总和必定为0,分给k个,那么必然也是0,糖果的总数也是0,所以将dp[0][0]初始化为0
(5)dp数组的遍历顺序:注意看递推公式:由它的左上角推出,所以只需正序遍历两层即可
(6)dp数组的返回值:由于题目要求的能整除k的糖果总数,那么根据定义,j一定为0,要使糖果的总数最大,那么一定是从前n个包中选,所以返回值是dp[n][0]
#include
#include
using namespace std;
const int N = 110;
int n, k;
int f[N][N];
int max(int x, int y)
{
return x > y ? x : y;
}
int main()
{
scanf("%d%d", &n, &k);
memset(f, -0x3f, sizeof f);
f[0][0] = 0;//所有从前i个物品中选,且总和除以k为j的糖果数量
//求最大数量
for (int i = 1; i <= n; i++)//外层遍历物品
{
int w;
scanf("%d", &w);
for (int j = 0; j < k; j++)//内层遍历背包容量
{ //不选:等于上一个的状态 //选:
f[i][j] = max(f[i - 1][j], f[i - 1][(j + k - w % k) % k] + w);
}
}
printf("%d\n", f[n][0]);//寻找余数为0的,即能别整除的
return 0;
}
1222. 密码脱落 - AcWing题库
题目概述:
给定一个字符串,请判断原来的字符串通过多少次种子脱落,而得到的现在的字符串
题意转化:
由于脱落的字母可以从任意位置脱落,所以只需求出给定字符串的最大回文串,那么用给定字符串的长度--该串中的最大回文串,那么即可得到孤立无援的字母,那么再对应从任意位置中加入该字母,即可凑成原来的回文串
区间DP:
(1)dp数组的含义:在{i,j}区间内的范围内,最大回文串的长度
(2)dp数组的属性:长度
(3)dp数组的递推公式
if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2;(如果左右端点相等,那么就把这两个端点加入)
(左右两边向外扩张)
if (f[l][r - 1] > f[l][r]) f[l][r] = f[l][r - 1];(寻找最大的回文子串)if (f[l + 1][r] > f[l][r]) f[l][r] = f[l + 1][r];(寻找最大的回文子串)
(4)dp数组的初始化:对于单个字母,独立成一个长度为1的回文串,所以遍历长度的同时,将len==1,全部赋值为dp[i][j]=1
(5)dp数组的遍历顺序:滑动窗口的思想,先枚举窗口的大小,再枚举左端点,边界条件为右端点小于数组的长度即可
(6)dp数组的返回值:最长的回文子串一定包含在dp[0][n-1](注意定义)
#include
#include
using namespace std;
const int N = 1010;
char s[N];
int f[N][N];
int main()
{
scanf("%s", s);
int n = strlen(s);
for(int len=1;len<=n;len++)
for (int l = 0; l + len - 1 < n; l++)
{
int r = l + len - 1;
if (len == 1) f[l][r] = 1;
else
{
if (s[l] == s[r]) f[l][r] = f[l + 1][r - 1] + 2;
if (f[l][r - 1] > f[l][r]) f[l][r] = f[l][r - 1];
if (f[l + 1][r] > f[l][r]) f[l][r] = f[l + 1][r];
}
}
printf("%d\n", n - f[0][n - 1]);//得到有多少个单独的字母个数,那么就只需再+上即可配对
return 0;
}
1070. 括号配对 - AcWing题库
题意概述:给定括号序列,通过在任意位置添加括号序列使得该括号序列合法
本题与上题(密码脱落)不同的点在于:不能单纯地通过判断左右端点满足括号的条件就同时向外扩展,要注意特例:比如说: [ ] ( [ ]
dp分析:
(1)dp数组的含义:向字符串[i][j]范围内通过添加括号,使其变成合法括号的集合
(2)dp数组的属性:添加括号的最小操作数
(3)dp数组的递推公式:
<1>首先应该确定一个判断其合法的函数,即前面为 [ ( 那么后面就应该为 ) ]
<2>如果满足上述的判断,且i,j的间距小于等于1,那么相当于这个是确实符合条件了,没有特例,可同时向左右两边扩展
<3>如果满足上述的判断,但是i,j的间距大于1,那么就要在[i,j]范围内枚举间隔点,将其分割为[i,k] [k+1,r],这里是找到最小操作数的核心
(4)dp数组的初始化:对于单独的括号,最小操作数就是添加一个与其相对应的括号,即最小操作数是1,对于没有意义的地方,由于是求最小,所以初始化为最大
(5)dp数组的遍历顺序:一个从后向前遍历,一个从前向后遍历,这样是保证【i,j】是左右端点的同时,减少一些不必要的重复操作,当然也可以i从前,j=i,向前遍历
(6)dp数组的返回值,同理:答案必定存在于整个字符串中
#include
#include
#include
#include
using namespace std;
const int N=110;
int dp[N][N];
char s[N];
bool match(int i,int j)
{
if(s[i]=='[' && s[j]==']') return true;
if(s[i]=='(' && s[j]==')') return true;
return false;
}
int main()
{
scanf("%s",s);
int n=strlen(s);
for(int i=0;i-1;i--)
{
for(int j=i;j
生命之树(树形DP)
1220. 生命之树 - AcWing题库
树形DP:
(1)dp数组的定义:表示以u为根节点的子树中所有连通块的的权值的最大值
(2)dp数组的属性:最大值
(3)dp数组的递推公式:如果大于0,则把它加入dp数组中,核心就是贪心的思想,因为要求的是连通块的最大值
核心是理解下面这个图以及dp数组的含义
dp[1]是:以1为根节点,包括1的所有联通块的最大值
dp[2]是:以2为根节点,包括2的所有连通块的最大值(注意此时不包括1)
dp[3]是:以3为根节点,包括3的所有连通块的最大值(注意此时不包括1)
同理dp[4]=4 dp[5]=5(也就是说不能向上寻找只能向下寻找)
所以dfs深搜的第一步是:将f[u]=w[u] (将该根节点的值存入dp数组)
之后才是向下搜索,注意此时的f[j]只是可能是的单个节点的值,它指的是这个节点之下包括这个节点的值,所以是大于0,才把它加入进去
#include
#include
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int n;
LL f[N];
int w[N];
int h[N], e[N * 2], ne[N * 2], idx;
void add(int a,int b){
e[idx] = b;
ne[idx] = h[a];
h[a] = idx ++;
}
//第二个参数是记录父节点,防止向回走
void dfs(int u,int fa){
f[u] = w[u];
for (int p = h[u] ; p != -1 ; p = ne[p])//单链表的遍历
{
int j = e[p];
if (j != fa)//防止走回头路
{
dfs(j, u);//取出节点所存的值作为下一个的当前节点,当前节点作为下一个节点的父亲
f[u] += max(0ll, f[j]);//连通块++
}
}
}
int main()
{
//读入
cin >> n;
for (int i = 1 ; i <= n ; i ++) cin >> w[i];
memset(h, -1, sizeof h);
for (int i = 0 ; i < n - 1 ; i ++)
{
int a, b;
cin >> a >> b;
add(a, b);add(b, a);//无向图
}
//树形DP
dfs(1, -1);//当前节点,当前节点的父亲
LL res = -1e10;
for (int i = 1 ; i <= n ; i ++) res = max(res, f[i]);
cout << res << endl;
return 0;
}
旅游规划(树形DP)
1078. 旅游规划 - AcWing题库
题意转换:求树的直径上的点
在活动 - AcWing(大臣的旅费)中,我们知道了一种求树的直径的方法:
代码实现:
#include
#include
#include
#include
#include
using namespace std;
const int N = 100010;
int n;
struct Edge
{
int id, w;//指向的那条边,和这条边的边长
};
vector h[N];
int dist[N];
void dfs(int u, int father, int distance)
{
dist[u] = distance;//记录长度
for (auto node : h[u])//想象成单链表
{
if (node.id != father)//保证不会往回走,保证向下走
dfs(node.id, u, distance + node.w);//当前节点,u则为当前节点的父亲,距离增加
}
}
int main()
{
scanf("%d", &n);
for (int i = 0; i < n - 1; i++)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
h[a].push_back({ b,c });
h[b].push_back({ a,c });
}
dfs(1, -1, 0);//第一次传参:当前节点,当前节点的父亲,当前节点到父节点的距离
int u = 1;
for(int i=1;i<=n;i++)
if (dist[i] > dist[u])//寻找从1号点到最远能到的路径距离
{
u = i;
}
dfs(u, -1, 0);//上述的最远的能到的距离作为头,从新开始寻找离它的最远距离
for(int i=1;i<=n;i++)
if (dist[i] > dist[u])//同理
{
u = i;
}
int s = dist[u];//u到最远的距离
printf("%lld\n", s * 10 + s * (s + 1ll) / 2);
return 0;
}
而在本题中,采用了另外一种做法,因为要求树的直径经过的点:
对于一个节点而言,要证明它是否在树的直径上,那么只需证明以它为节点,它的最大值和次大值的和是否等于树的直径即可,下面就是对跟节点的不同而分的情况:
<1>当树为一开始的根节点(头部)时即:
那么即是它:向下寻找的最大值和次大值之和,此时向下的最大值就是d1,次大值就是d2
注意:次大值不一定比最大值小,它是除了最大值以外,最大的数
<2>当树为中间的根节点时即:
那么向下扩展的同时,也需要向上扩展,此时就是数组up,同时需要记录:
如果j向下走
因为d1[j]是不能往回走的(一开始是从i-->j),所以此时是要用d2[j]来向下走
如果不是已经走过的点,那么就可以用d1[j]继续走
#include
#include
#include
#include
using namespace std;
const int N = 200010, M = N * 2;
int h[N], e[M], ne[M], idx;
int n, maxd;
int d1[N], d2[N], up[N], p1[N];
//d1:每个节点向下走的最长路径
//d2:每个节点向下走的次长路径的长度
//up数组:表示每个节点向上走能走的最远距离
//p1存下u节点往下最大路走的子节点
//所有结点的最长路径和次长路径之和的最大值就是树的直径
void add(int a, int b)
{
e[idx] = b;
ne[idx] = h[a];
h[a] = idx++;
}
void dfs_d(int u, int father)//向下寻找
{
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (j != father)//防止向父亲方向寻找(无向图:每个节点方向都有可能是父亲方向)
{
dfs_d(j, u);//向下一层寻找
int distance = d1[j] + 1;//距离+1
if (distance > d1[u])//如果距离大于了当前向下寻找的最大距离
{
d2[u] = d1[u];//次大节点距离=上一个最大节点的最大距离
d1[u] = distance;//最大距离更新为distance
p1[u] = j;//记录已经走过的节点
}
else if (distance > d2[u]) d2[u] = distance;
}
}
maxd = max(maxd, d1[u] + d2[u]);
}
void dfs_u(int u, int father)//向上寻找
{
for (int i = h[u]; i != -1; i = ne[i])
{
int j = e[i];
if (j != father)
{
up[j] = up[u] + 1;//up数组和父节点有关
if (p1[u] == j) //说明从j向下走是从u向下走最长路径
{
up[j] = max(up[j], d2[u] + 1);//用第二长的路径更新up数组
}
else up[j] = max(up[j], d1[u] + 1);//说明从j向下走不是从u向下走的最长路径,用最长的路径更新up数组
dfs_u(j, u);//从上往下算
}
}
}
int main()
{
memset(h, -1, sizeof h);
scanf("%d", &n);
for (int i = 0; i < n - 1; i++)
{
int a, b;
scanf("%d%d", &a, &b);
add(a, b);//无向图
add(b, a);
}
dfs_d(0, -1);
dfs_u(0, -1);
for (int i = 0; i < n; i++)
{
int dx[3] = { d1[i],d2[i],up[i] };
sort(dx, dx + 3);
if (dx[2] + dx[1] == maxd) printf("%d\n", i);//如果这个点的扩展最大和次大的和==树的直径,那么这个点必然在直接上
}
return 0;
}
你可能感兴趣的:(AcWing蓝桥杯,蓝桥杯,c++,动态规划,算法,深度优先)