文章内容概要
本次文章将会讲算法中的搜索,数据结构(进阶)和动态规划。这几个内容在蓝桥杯中非常的常考,建议大家认真阅读。
下期将会为大家讲解图论相关的知识,也将是基础算法的最后一个部分,把这个部分讲完之后,就应该进去刷题环节了,博主每周也会上传一些自己遇到的比较好的题目
搜索
搜索也叫做暴搜,在未优化前就是通过穷举所有情况来找到最优解
搜索一般分为深度优先搜索和宽度优先搜索
一般用到的优化方法是:回溯和剪枝
回溯:在搜索过程中,遇到走不通或者走到底的情况时,就回头
剪枝:在搜索过程中,剪掉重复出现或者不是最优解的分支
用的数不重复的用排列组合思想去分析题(像eg:高中的C和A类型的题)
深度优先搜索-DFS(递归型枚举)
实现方法:(用全局变量标记每一步干了啥)+回溯来实现dfs
辅助理解:画决策树
递归型枚举这类题的数据范围都很小--可以当做一个题眼
洛谷 B3622 枚举⼦集(递归实现指数型枚举)
洛⾕ P1706 全排列问题
洛谷 B3623 枚举排列(递归实现排列型枚举)
c++
1.例题: 洛谷 B3622 枚举⼦集(递归实现指数型枚举)
#include <bits/stdc++.h>
using namespace std;
int n;
string path;//全局变量,记录递归过程中,每一步的决策
void dfs(int pos)
{
if(pos>n)
{
cout<<path<<endl;
return;
}
//不选
path+='N';
dfs(pos+1);
path.pop_back();//回溯,一般在这个eg:dfs(pos+1)后面会用
//其他类型的数据的话,一般用vector存,好尾删
//选
略
int main()
{
cin>>n;
dfs(1);
return 0;
}
2.排列枚举
例题: 洛谷 B3623 枚举排列(递归实现排列型枚举)
洛⾕ P1706 全排列问题
都需要用到 bool st[N]去标记哪些已经选过了
一般会在回溯后面加上st[i]=false(表示选过了);//这是剪枝
剪枝与优化(dfs的)
洛谷 P10483 ⼩猫爬⼭
洛谷 P1464 Function
在dfs中,有几种常见的剪枝方法:
1.排除等效冗余:
如果在搜索过程中,通过某⼀个节点往下的若⼲分⽀中,存在最终结果等效的分⽀,那么就只需要搜索其中⼀条分⽀。
(比如:组合中的12和21)
2.可行性剪枝:
如果在搜索过程中,发现有⼀条分⽀是⽆论如何都拿不到最终解,此时就可以放弃这个分⽀,转⽽搜索其它的分⽀。
(比如:组合中的11)
3.最优化剪枝:(在找最优解时会用)
如果在搜索过程中,发现某⼀个分⽀已经超过当前已经搜索过的最优解,那么这个分⽀往后的搜索,必定不会拿到最优解。此时应该停⽌搜索,转⽽搜索其它情况。
4.优化搜索顺序:(在找最优解时会用)
在有些搜索问题中,搜索顺序是不影响最终结果的
此时,应当先选择⼀个搜索分⽀规模较⼩的搜索顺序,快速拿到⼀个最优解之后,⽤最优性剪枝剪掉别的分⽀。
例题: 洛谷 P10483 ⼩猫爬⼭
5.记忆化搜索:(有非常多完全相同的子问题时用此)
(可以通过增加形参来让子问题变得相同)
记录每⼀个状态的搜索结果,当下⼀次搜索到这个状态时,直接找到之前记录过的搜索结果。这也解决了以前遇到大量重复运算不能用递归的场景
例题: 洛谷 P1464 Function
c++
记忆化搜索的注意事项:
1.备忘录中不能一开始就出现递归过程中有可能出现的值
2.递归返回的时候,先把值先存到备忘录里面
3.递归的时候,先往备忘录里面瞅一瞅(不要用成还没初始化的值了)
温馨提示:有些题对剪枝的位置也要要求(可从树状图看出)
例题:洛谷 P1025 [NOIP2001 提⾼组] 数的划分
宽度优先搜索(BFS)
BFS常用来解决边权为1的最短路问题
eg:在二维中至少走多少步才能到...
c++
bfs题在写代码的时候:(dfs才是递归)
常用 queue<pair<int,int>>q来存坐标
表示走了多少步的int dist[N][N];
在while(q.size())里面去循环
多源BFS
当问题中存在多个起点⽽不是单⼀起点时,这时的最短路问题就是多源最短路问题。
在多源最短路问题中,边权为1的话就可以用多源BFS
c++
把这些源点汇聚在⼀起,当成⼀个"超级源点"就变成了单源BFS
即 1.初始化的时候,把所有的源点都加⼊到队列⾥⾯;
2. 然后正常执⾏ bfs 的逻辑即可。
例题:牛客网 矩阵距离
01 BFS
感觉跟背包那的01问题差距还是大
这个01BFS是"走路"问题
在BFS过程中,把边权为0的扩展出来的点放在队首,把边权为1的扩展出来的点放到队尾(核心思想)
01BFS相较于其他BFS,如果遇到已经遍历过的结点,有可能会找到一条更优的路径
(上面的核心思想体现了这个)
c++
例题 洛谷 P4554 ⼩明的游戏
Floodfill问题
本质是在寻找具有相同性质的联通块
洛谷 P1596 [USACO10OCT] Lake Counting S
c++
例题: 洛谷 P1596 [USACO10OCT] Lake Counting S
其中的主要代码:
dx[] dy[]是可以走的那几个方向
//给联通的地方打上标记
void dfs(int i, int j)
{
st[i][j] =true;
for(int k = 0;k<0;k++)
{
int x = i + dx[k], y = j + dy[k];
if(x >= 1 && x <= n && y >= 1 && y <= m && a[x][y] == 'W' && st[x][y] == false)
dfs(x,y)
}
}
int main()
{
...
if(a[i][j]=='W'&&st[i][j] ==false)
{
ret++;
dfs(i,j);
}
}
数据结构(进阶)
单调栈
里面存储的单增或者单减的栈
应用:
1.寻找当前元素左侧,离它最近,并且比它大的元素在哪
2.寻找当前元素左侧,离它最近,并且比它小的元素在哪
3.寻找当前元素右侧,离它最近,并且比它大的元素在哪
4.寻找当前元素右侧,离它最近,并且比它小的元素在哪
c++
寻找当前元素左侧,离它最近,并且比它大的元素在哪
int a[N]里面存数(已存好)
ret里面存的答案
stack<int>st;//维护一个单调递减的栈,里面存的是下标
for(int i = 1;i<=n;i++)
{
//栈里面小于等于a[i]的元素全部出栈
while(st.size()&&a[st.top()]<=a[i]) st.pop();
//此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i]=st.top();
st.push(i);
}
寻找当前元素右侧,离它最近,并且比它大的元素在哪
改成for(int i = n;i>=1;i--)
c++
寻找当前元素左侧,离它最近,并且比它小的元素在哪
int a[N]里面存数(已存好)
ret里面存的答案
stack<int>st;//维护一个单调递增的栈,里面存的是下标
for(i=1;i<=n;i++)
{
//栈里面大于等于a[i]的元素全部出栈
while(st.size()&&a[st.top()]>=a[i])st.pop();//这里的符号是跟上面的唯一区别
//此时栈顶元素存在,栈顶元素就是所求结果
if(st.size()) ret[i]=st.top();
st.push(i);
}
寻找当前元素右侧,离它最近,并且比它小的元素在哪
把上面改为for(int i=n;i>=1;i--)即可//n次逆遍历可以记一下是这个
c++
总结:
找左侧,正遍历;找右侧,逆遍历;
比它大,单调减;比它小,单调增。//构造___栈
例题: 洛谷 P1901 发射站
单调队列
是一个存单调元素的双端队列
应用:解决滑动窗口内的最大值最小值问题
(前面基础算法的滑动窗口不是用来求其中的最值的)
c++
例题:洛谷 P1886 滑动窗⼝ /【模板】单调队列
int a[N]里面存的元素(已存好)
窗口内最大值:
从左往右遍历元素,维护一个单调递减的双端队列
当前元素进队之后,注意维护队列内的元素在大小为k的窗口内
此时队头元素就是最大值
代码:
deque<int>q;//存下标
for(int i = 1;i<=n;i++)
{
while(q.size()&&a[q.back()]<=a[i])q.pop_back();
q.push_back(i);
if(q.back()-q.front()+1>k) q.pop_front();
if(i>=k)cout<<a[q.front()]<<" ";
}
窗口内最小值:
从左往右遍历元素,维护一个单调递增的双端队列
当前元素进队之后,注意维护队列的元素在大小为k的窗口内
此时队头元素就是最小值
并查集
并查集是一种用于维护元素所属集合的数据结构,用双亲表示法来实现
应用eg:维护连通块的信息
相关的一些概念:
查询操作:查找元素x属于哪一个集合,一般会在每个集合中选取一个元素作为代码,查询的是这个集合中的代表元素
合并操作:将元素x所在的集合与元素y所在的集合合并成一个元素
(注意:合并的是元素所属的集合,而并非单单两个元素)
判断操作:判断元素x和y是否在同一个集合
c++
并查集的实现:
例题:洛谷 P1551 亲戚
1.初始化:让元素自己指向自己即可
int fa[N];一般并查集用fa来表示
for(int i=1;i<=n;i++) fa[i] = i;
2.查询操作:就是一直向上"找爸爸"(这个find可以多次使用)
int find(int x)//查询x所在集合的代表元素是谁
return fa[x] == x?x:find(fa[x]);//是x就返回x,不是就find(fa[x])
3.合并操作:(重复合并不会出错)
让元素x所在集合的代表元素指向元素y所在集合的代表元素
void un(int x,int y)
{
int fx = find(x);
int fy = find(y);
fa[fx] = fy;
}
4.判断操作
看看两者所在集合的总代表元素是否相同
bool issame(int x,int y)
{
return find(x) == find(y);
}
扩展域并查集
元素之间存在多种关系比如:朋友和敌人 而不像上面只存在:亲戚这样一种关系的话,就要用扩展域并查集
和普通并查集的区别:将每个元素拆分成多个域,每个域代表⼀种状态或者关系
c++
例题:洛谷 P1892 [BOI2003] 团伙
这里只写不同点:
find和un与上面的写法一模一样
//两种关系,所以N*2:x的母敌人是x+n
int fa[N*2];
//初始化:
for(int i=1;i<=n*2;i++) fa[i]=i;
while(m--)//m是题目中的"m个关系"
{
if(op == 'F') un(x,y);
else//敌人,读取的是y和x是敌人关系
{//存法:这俩都得写,少一个都不行
un(x,y+n);//这两是朋友关系
un(y,x+n);//这两是朋友关系
}
}
带权并查集
概念:就是在普通并查集的基础上,为每个结点增加一个权值。这个权值可以表示当前结点与父结点之间的信息(因为find那我们用的路径压缩,因为最终这个权值是当前结点相对于根节点的信息)
c++
带权并查集的一些操作:(这里的d[N]以到父结点的距离为例)
例题:洛谷 P1196 [NOI2002] 银河英雄传说
新加了一个d[N]
1.初始化:
多初始化个d[i]即可
2.查询操作:(对这种代码来说,一个结点只能进行一次这种find操作!)
int find(int x)
{
if(fa[x]==x) return x;
int t = find(fa[x]);//这一步要先搞
d[x]+=d[fa[x]];//不为距离时可能会变为其他
return fa[x] = t;//完成路径压缩
}
3.合并操作://x所在集合与y所在集合合并,x与y之间的权值是w
void un (int x,int y, int w)
{
int fx = find(x),fy = find(y);
if(fx!=fy)
{
fa[fx] = fy;
d[fx]= d[y]+w-d[x]; //可能会变,这里是拿距离举例的
}
}
4.查询操作://查询y与x之间的距离
int query(int x,int y)
{
int fx = find(x),fy = find(y);
//如果不在同一个集合中,说明距离未知(具体返回什么要看题意)
if(fx!=fy)return 0x3f3f3f3f;
return d[y]-d[x];
}
字符串哈希
一般利用前缀和思想去预处理字符串汇总每个前缀的哈希值
(使用前提:需要多次询问一个字符串的子串的哈希值)
核心思想:把字符串映射成P进制数字
P一般选择13331或者131
字符串哈希的作用:
字符串哈希一般用于判断两个字符串及其各子串是否相等
(和字典树的区别是这个字符串哈希侧重于判断功能)
c++
例题:洛谷 P10468 兔⼦与兔⼦
前缀字符串哈希模板:
typedef unsigned lon long ULL;
P = 13331;
int len;
string s;//一般都要搞一下s = ' '+s;来让i从1开始搞
ULL f[N];//前缀哈希数组
ULL p[N];//记录P的i次方->p[i]为P的i次方
//处理前缀哈希数组以及P的i次方数组
void init_hash()
{
f[0] = 0;p[0] = 1;
for(int i = 1;i<=len;i++)
{
f[i]=f[i-1]*P+s[i];
p[i]=p[i-1]*P;
}
}
//快速求得任意区间的哈希值
ULL get_hash(int l,int r)
{
return f[r]-f[l-1]*p[r-l+1];
}
如果题目只是简单的求单个字符串的哈希值:
ULL gethash()
{
ULL ret = 0;
for(int i =1;i<=len;i++)
{
ret = ret*p+s[i];
}
return ret;
}
Trie树(又叫字典树或前缀树)
是一种能够快速插入和查询字符串的数据结构
理解:我们可以把字典树想象成一颗多叉树,每一条边代表一个字符,从根节点到某个节点的路径就代表了一个字符串
作用:
1.查询某个单词是否出现过,并且出现几次
2.查询有多少个单词是以某个字符串为前缀
3.查询所有以某个前缀开头的单词
c++
字典树的实现:
默认需要存的全是小写字母
1.准备工作
int tree[N][26],p[N],e[N];//N一般令为所有字符串中一共有多少个字符
// p[i]表示第i个被开辟的点被经过了多少次,
//e[i]表示以第i个被开辟的点为结尾的有多少个
int idx;
2.插入字符串
void insert(string&s)
{
int cur = 0;//从根节点开始
p[cur]++;//这个格子经过一次
for(auto ch:s)
{
//这个path的表达式常改!!!依据题意改
int path = ch - 'a';
//如果没有路
if(tree[cur][path] == 0) tree[cur][path]= ++idx;
cur = tree[cur][path];
p[cur]++
}
e[cur]++;
}
3.查询字符串出现的次数:
int find(string&s)
{
int cur = 0;
for(auto ch:s)
{
int path = ch - 'a';
if(tree[cur][path] == 0) return 0;
cur = tree[cur][path];
}
return e[cur];
}
4.查询有多少个单词以字符串s为前缀:
int find_pre(string&s)
{
int cur = 0;
for(auto ch:s)
{
int path = ch-'a';
if(tree[cur][path] == 0) return 0;
cur = tree[cur][path];
}
return p[cur];
}
例题:洛谷 P2580 于是他错误的点名开始了
动态规划
前言
在竞赛中,如果遇到动态规划的题目,只要不是经典题型,那么大概率就是以压轴题的形式出现
用动态规划解决问题的步骤:(递推形式)
1.定义状态表示:
根据经验+需要的意义,赋予dp数组相应的含义
(主要还是看需要记什么)
2.推导状态转移方程:
在dp表中分析,当前格子如何通过其余格子推导出来的
3.初始化:
将显而易见的以及边界情况下的位置填上值,来让后续填表的结果是正确的
4.确定填表顺序:
根据状态转移方程,确定按照什么顺序来填表
5.找出最终结果:
在表中找出需要的最终结果
洛谷 P10250 [GESP样题 六级] 下楼梯
洛谷 P1216 [USACO1.5] [IOI1994]数字三⻆形 Number Triangles
c++
动态规划常要空间优化:
即把不用了的值给不要了
但是背包问题如果不能用1个数组优化,而要多个数组的话,那就不空间优化了,否则可能会超时
常见的优化方法:
方法一:一维转几个数
例题:洛谷 P10250 [GESP样题 六级] 下楼梯
方法二:二维转一维
1.是否需要修改遍历顺序
2.删掉一维即可
例题:洛谷 P1216 [USACO1.5] [IOI1994]数字三⻆形 Number Triangles
洛谷 P1115 最⼤⼦段和
洛谷 P1541 [NOIP2010 提⾼组] 乌⻳棋
c++
一些技巧:
1.状态表示:
a.研究子数组或子序列问题时,从某个位置结尾来定义状态表示
例:洛谷 P1115 最⼤⼦段和
b.优化问题:有一维可以由其他维的数据推出的话,可以不要这一维(不是指那个空间优化方法)
例题:洛谷 P1541 [NOIP2010 提⾼组] 乌⻳棋
2.状态转移方程:
a.求方案数一般是相加
b.常从最后一步开始分析去推导状态转移方程
3.初始化:
在求方案数时,一般将第一个位置初始化为1,其他位置为0
线性dp
特点:状态转移只依赖于前一个或前几个状态;状态之间的关系是线性的
路径类dp
c++
走两次的得到的值之和最大问题:
(两次的规则要一样,走到不同位置的得到的值不一样,且每位置的值只能得一次)
例题:洛谷 P1004 [NOIP2000 提⾼组] ⽅格取数
在状态表示时:两者如果是同时出发的(非同步则要改一点),其横纵坐标之和会是一个定值
一般表示为f[st][i1][i2]:
意思是:第一条路在[i1,st-i1],第二条路在[i2,st-i2]时,两者的路径(取数)之和最大
经典线性dp
经典线性dp问题有两个:最长上升子序列和最长公共子序列
这两道题的解题思路,定义状态表示方式和推导状态转移方程的技巧常被用到其他题目中去
洛谷 B3637 最⻓上升⼦序列
洛谷 P1091 [NOIP2004 提⾼组] 合唱队形
c++
最长上升子序列(数据范围小时用此)
时间复杂度是O(n平方)
链接:洛谷 B3637 最⻓上升⼦序列
要理解最长上升子序列是什么意思!!!
1.状态表示:
dp[i]表示:以i位置为结尾的所有子序列中,最长递增子序列的长度
2.状态转移方程:
根据子序列的构成方式进行分类讨论:
第一种:子序列长度为1:此时的dp[i] = 1;
第二种: 子序列的长度大于1:设j为前面某一个数的下标
只要a[j]<a[i],i位置元素跟在j元素后面就可以形成递增序列
其dp[i] = max(dp[j]+1p,dp[i])
3.初始化:
每次填表的时候,先把这个位置的数改成最小的域值即可
主要部分的代码展示:
int ret = 0;
for(int i=1;i<=n;i++)
{
dp[i]=1;
for(int j=1;j<i;j++)
{
if(a[j]<a[i])
dp[i] = max(dp[i],dp[j]+1);
}
ret = max(ret,dp[i])
}
应用: 洛谷 P1091 [NOIP2004 提⾼组] 合唱队形
c++
最长上升子序列(数据范围大时用此)
时间复杂度时O(n*logn)
链接:牛客网 【模板】最⻓上升⼦序列
优化动态规划:
1.发现在考虑最长递增子序列长度时,只用关心现在长度和序列最后一个元素
因此仅需统计长度为x的所有递增序列中最后一个元素的最小值(创建数组去统计)
2.在统计过程中发现:
数组中的数呈现递增趋势,因为可以使用二分来查找插入位置
主要部分的代码展示:
for(int i =1;i<=n;i++)
{
//处理边界情况
if(len ==0||a[i]>f[len]) f[++len]=a[i];
else
{
//二分插入位置
int l = 1,r = len;
while(l<r)
{
int mid = (l+r)/2;
if(f[mid]>=a[i]) r = mid;
else l =mid+1;
}
f[l] = a[i]
}
}
c++
牛可乐和最长公共子序列
链接:牛客网 ⽜可乐和最⻓公共⼦序列
1.状态表示:
dp[i][j]表示:
s1的[1,i]区间以及s2的[1,j]区间内的所有的子序列中,最长公共子序列的长度
2.状态转移方程:
对于dp[i][j],我们可以根据s1[i]与s2[j]的字符分情况讨论:
第一种:两个字符相同 dp[i][j] = dp[i-1][j-1]+1
第二种:两个字符不同 有以下三种策略
a.去s1的[1,i-1]以及s2的[1,j]区间内找:此时最大长度为dp[i-1][j]
b.去s1的[1,i]以及s2的[1,j-1]区间内找:此时最大长度为dp[i][j-1]
c.去s1的[1,i-1]以及s2的[1,j-1]区间内找:此时最大长度为dp[i-1][j-1]
(发现c包含在a和b情况里)
然后要a,b中的最大值即可
代码展示:
for(int i =1;i<=n;i++)
{
for(int j = 1;j<=m;j++)
{
if(s[i-1] == t[j-1])f[i][j] = f[i-1][j-1]+1;
else f[i][j] = max(f[i-1],f[i][j-1])
}
}
应用:洛谷 P2758 编辑距离
背包问题分类
01背包问题:没中物品只能选或不选(选0次或1次)
完全背包问题:每种物品可以选择无限次
多重背包问题:每种物品有数量限制
分治背包问题:物品被分为若干组,每组只能选一个物品
混合背包:以上四种背包问题混在一起
多维费用的背包问题:限定条件不止有体积,还会有其他因素(比如重量)
c++
背包问题的三种限定情况:
1.体积不超过j->小于等于j->j>=v[i]
2.体积正好为j->恰好等于j->要判断f表中某些状态是否合法
3.体积至少为j->大于等于j->j<v[i]也是合法的,映射到0位置即可
背包问题在求方案数时,要把不选的情况单独写出,然后从只选一个开始,不然套模板可能会错
例:洛谷 P1077 [NOIP2012 普及组] 摆花
01背包问题
牛客网 【模板】01背包
洛谷 P2946 [USACO09MAR] Cow Frisbee Team S
c++
01背包中的最大价值问题:
模板题:牛客网 【模板】01背包
v[i]表示第i个的体积 w[i]表示第i个的价值
第一问:求这个背包至多能装多大价值的物品
1.状态表示:
dp[i][j]表示:从前i个物品中挑选,总体积不超过j,所有的选法中,能挑选出来的最大价值
那么dp[n][v]就是我们要的结果
2.状态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i-1][j-v[i]]+w[i])
3.初始化:无
4.填表顺序:从上往下(二维一般都是从上往下) 从右往左
代码表现:
for(int i = 1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
f[i][j] = f[i-1][j];
if(j>=v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
空间优化版:(熟练了之后,直接写此)
for(int i = 1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)//01背包空间优化要修改遍历顺序
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
}
第二问:
若背包恰好装满,求至少能装多大价值
仅需要在第一问基础上修改一下初始化以及最终结果即可
1.初始化
把所有位置先设置成-0x3f3f3f3f;然后把dp[0][0]修改成0
2.最终结果
要多一步判断最后一个位置是不是小于0
代码展现:
memset(f,-0x3f,sizeof f);//多了这个
for(int i = 1;i<=n;i++)
{
for(int j=0;j<=m;j++)
{
f[i][j] = f[i-1][j];
if(j>=v[i])
f[i][j] = max(f[i][j],f[i-1][j-v[i]]+w[i]);
}
}
if(f[n][m]<0)cout<<0<<endl;//多了这个
else cout << f[n][m]<<endl;//多了这个
空间优化版:(熟练了之后,直接写此)
memset(f,-0x3f,sizeof f);
for(int i = 1;i<=n;i++)
{
for(int j=m;j>=v[i];j--)//01背包空间优化要修改遍历顺序
{
f[j] = max(f[j],f[j-v[i]]+w[i]);
}
}
if(f[m]<0)cout<<0<<endl;
else cout << f[m]<<endl;
注意:有些题可能不能用这种空间优化,因为要用到之前的数据的多少不同了
例题:洛谷 P2946 [USACO09MAR] Cow Frisbee Team S
(这题还要注意的是状态表示不能搞能力值总和为j,要搞总和模之后为j,不然空间时间都易超)
c++
01背包中的方案数问题:
题目链接:洛谷 P1164 ⼩A点菜
1.状态表示:
dp[i][j]表示:从前i个菜中挑选,总价钱恰好等于j,此时的总方案数
2.针对于a[i]选或不选,分两种情况讨论:
得:dp[i][j] = d[i-1][j]+d[i-1][j-a[i]]
注意第二个状态可能不存在,要注意判断一下j>=a[i]
3.初始化:
dp[0][0] = 1
完全背包问题
c++
模板题:牛客网 【模板】完全背包
第一问:求这个背包至多能装多大价值的物品
1.状态表示:和01背包一样
2.状态转移方程:
dp[i][j] = max(dp[i-1][j],dp[i][j-v[i]]+w[i])
3.初始化:
从第二行开始存,第一行全初始化为0
4.没有空间优化时,是从上往下,从左往右
第二问:若背包恰好装满,求至多能装多大价值的物品
改法跟01背包那里完全相同
与01背包的不同点:在空间优化时,完全背包的遍历顺序还是从左往右
多重背包
c++
例题: 牛客网 多重背包
小tip:不是所有的多重背包问题都能用二进制优化,而且优化版本的代码很长
如果时间复杂度允许的情况下,能不优化就不优化
解法一:
1.状态表示:
dp[i][j]表示:
从前i个物品中挑选,总重量不超过j的情况下,最大的价值
2.状态转移方程:
这里不能用完全背包的方式(不是指那个空间优化)去优化
只能硬分x[i]+1种情况(选0个到选x[i]个)
选x[i]个:价值为dp[i-1][j-x[i]*w[i]]+x[i]*v[i]
3.初始化:
全部初始化为0
4.填表顺序:
从上往下,从左往右
空间优化之后则是从右往左
代码展现:
for(int i =1;i<=n;i++)
for(int j= m;j>=0;j--)
for(int k = 0;k<=x[i]&&k*w[i]<=j;k++)
f[j] = max(f[j],f[j-k*w[i]]+k*v[i])
解法二:转化为01背包问题
优化方式:用二进制将同种的x[i]个物品分组
(如果是求方案数的话,是不能用二进制去优化的,会多算几种)
把这x[i]个物品拆成一些二进制数再加上多出来的数
此题的有100种不同物品的话
如果同种的物品最多能分成5份,则数组的N要取(100+10)*5//+10纯属个人习惯
c++
多重背包的方案数问题:
例题:洛谷 P1077 [NOIP2012 普及组] 摆花
也就状态转移方程和初始化改了一下
分组背包问题
c++
例题:洛谷 P1757 通天之分组背包
1.状态表示:
dp[i][j]表示从前i组中挑选物品,总重量不超过j的情况下,最大的价值
2.状态转移方程:
根据第i组选什么物品,可以分若干情况讨论
这个在空间优化时,第二维也要从右到左
混合背包问题
c++
例题:洛谷 P1833 樱花
这个混合背包的话,分情况讨论是哪种背包就是
注意的点是:多重背包和01背包的代码具有可合并性
多维费用的背包问题
c++
例题:洛谷 P1910 L 国的战⽃之间谍
这无非就是多加几维就行
eg:
状态表示:
dp[i][j][k]表示:
从前i个人中挑选,伪装能力不超过j,总工资不超过k,此时能获取到的最多资料总数
eg:像这种获取到的最多资料总数,能砍到的最多的树木这种一般不为限制条件,一般是所求的值
在空间优化时,也只能优化掉第一维(从前i个人中挑选--这个一般放第一维)
空间优化后,除了第一维的遍历顺序可能都需要发生变化
区间dp
区间dp是用区间的左右端点来描述状态,通过小区间的解来推导出大区间的解
核心思想:将大区间划分为小区间,其状态转移方程通常依赖于区间的划分点
常见的划分点的方式有两个:
1.基于区间的左右端点,分情况讨论
2.基于区间上的某一个点,划分成左右区间讨论
应用:在序列中只关心左右两端,大概率用区间dp
c++
例题:洛谷 P1435 [IOI2000] 回⽂字串
1.状态表示:
dp[i][j]表示:
字符串[i,j]区间,变成回文串的最小插入次数
那么,dp[1][n]就是我们要的结果
2.状态转移方程:
根据区间的左右端点,分情况讨论
对于区间dp的填表顺序,一般用的是:
第一维循环:从小到大枚举区间长度len;第二维循环:枚举区间左端点i,然后计算出区间右端点j
j = i+len-1
这个len有时从1开始,有时从2开始,看题
eg:
for(int len = 1;len<=n;len++)
for(int i = 1; i+len-1<=n;i++)
洛谷 P2858 [USACO06FEB] Treats for the Cows G/S
洛谷 P3146 [USACO16OPEN] 248 G
c++
关于这里的状态转移方程:
1.如果划分点是基于区间的左右端点,分情况讨论
eg: 洛谷 P2858 [USACO06FEB] Treats for the Cows G/S
先拿左边,然后去[i + 1, j] 区间获得最多的钱,
即a[i] × (n − len + 1) + dp[i + 1][j]
先拿右边,然后去[i,j-1]区间获得最多的钱
即a[j]*(n-len+1)+de[i][j-1]
2.如果划分点是基于区间上的某一个点,划分成左右区间讨论
eg: 洛谷 P3146 [USACO16OPEN] 248 G
根据最后一次合并的情况,可以把区间分成[i,k][k+1,j](划分点是k)
一些比较杂但是做题会用到的知识
c++
有时会用到大坐标的方法:
例题:洛谷 P1784 数独
eg: 本为a[i][j],然后搞一个b[i/3][j/3][num] = true;(举例:i,j为9)
表示在a中的数num在[i/3][j/3]这个3x3方格中
c++
如果一个坐标会一变多一直这样的话
可以用eg:queue<pair<int,int>> q;这些来存坐标
eg: 洛谷 P1443 ⻢的遍历
c++
二维坐标转一维的方法:(二维的下标最好从0开始)
可以将nxm的矩阵坐标(x,y),映射成一个数pos,可以起到空间优化的效果
公式: pos = x*m+y;
x =pos/m;
y = pos%m;

c++
常用思路:
正难则反
例题: 牛客网 矩阵距离
在此题表现为:
如果针对一个点,直接去找最近的1,那么就需要对所有0来一次bfs,但是时间复杂度太大了
因为去想从1向外扩展,每遍历到0就更新一下最短举例,这样就只用一次bfs
c++
消消乐思想:
如果要标记外围同数据(eg:都为0)但是这些数据又很分散的话,解决这个的方法:
我们可以把整个矩阵的外围包上⼀层0 ,这样只⽤从(0,0)位置开始搜即可
例题:洛谷 P1162 填涂颜⾊
c++
字符映射成连续数字的方法:
小写字母:0-25 ch-'a'
大写字母:26-51 ch-'A'+26
数字: 52-61 ch-'0'+52
应用eg;字典树那的path
c++
股票问题中的重要结论:
任意一笔跨天的交易,都可以转化成连续的"某天买,隔天卖"的形式
例题:洛谷 P5662 [CSP-J2019] 纪念品
c++
如果需要让列排序的话,必须要先转换成行,然后用sort
例题:洛谷 P5322 [BJOI2019] 排兵布阵
c++
for(int k = i;k<j;k++)这里循环j-i次
c++
处理环形问题的常用技巧:
倍增--即复写(这里的倍增不是指前面的倍增算法)
例题: 牛客网 丢手绢