2021/5/31
- 无向图的生成树(使用尽可能少的边将所有顶点连在一起)
- 包含所有顶点
- 任意两顶点之间有边构成唯一路径
- 百度百科定义:一个有 n 个结点的连通图的生成树是原图的极小连通子图,且包含原图中的所有 n 个结点,并且有保持图连通的最少的边
- 书上定义:极小连通子图为某一顶点子集所确定的连通子图中包含边最少的连通子图(n个顶点,无向连通图最少n-1条边,有向连通图最少n条边——成环)。图全部顶点所确定的极小连通子图即为连通图的生成树。即包含所有顶点的极小联通子图就是生成树。
- 在代码中实现,用并查集保持无环,且包含了所有节点即可。
- 蓝桥杯国赛凑平方数这题太经典了,将解题过程加以记录
- 涉及到dfs的优化、状态压缩、位运算
- 涉及到dfs的优化、状态压缩、位运算
#include <iostream>
#include <vector>
typedef long long ll;
using namespace std;
vector<int> state; //11001中1表示消耗了数字
int len; //合法平方数个数
void init() {
for (ll i = 0; i * i <= 9876543210l; i++) {
ll tmp = i * i;
int st = 0;
if (tmp == 0) {
st = 1;
state.push_back(st);
continue;
}
bool flag = true;
while (tmp) {
int t = tmp % 10;
tmp /= 10;
if (st & (1 << t)) { //存在1冲突
flag = false;
break;
} else
st |= (1 << t);
}
if (flag) {
state.push_back(st);
}
}
len = state.size();
}
//判断当前剩余数字能否实现num
bool check(int st, int numSt) {
return !(st & numSt);
}
ll ans = 0;
//在下标k~len-1范围内,找第一个合法的平方数
void f(int k, int st) { //st 0表示可用
if(st==(1<<10)-1){ //0~9都是1
ans++;
return;
}
for (int i = k; i < len; i++) {
if (check(st, state[i])) {
f(i + 1, st | state[i]);
}
}
}
int main() {
//将范围内的平方数都找出来,将其消耗的数字的信息,存入state中
init();
//递归
f(0, 0);
cout << ans;
return 0;
}
- 自己用递归实现全排列的好处在于,可以进行剪枝
- 今天发现了一个一直以来没发现的大问题:BFS找最短路径的时候,不用每个节点单独记录路径
- 所有节点共享一个set就行了,因为如果某个节点下一步走已经在集体公用set中的节点,则你这个节点必然已经落后了。
2021/5/30
-
今天写蓝桥杯国赛真题遇到2020十一届C++B组H题答疑这一题
- 作为B组倒数第二题,JavaC组最后一题,竟然经过一定的数学推导,代码比填空题第一题还简单。
-
看到一个对前缀和、树状数组、线段树的总结,写的不错
-
最长递增子序列
这道经典的算法题,用动态规划解法的时间复杂度是 O ( n 2 ) O(n^2) O(n2)- 使用一个队列,每轮不断优化其元素,使每个元素达到尽可能小(给后续递增留出最大的空间),其时间复杂度可达
O
(
n
log
n
)
O(n\log{n})
O(nlogn) (需要用到二分来查找插入位置,
lower_bound()
可以实现) - 这样只能算出来LIS的长度,最后的结果不一定是LIS
- 因为往前替换元素,违背了子序列顺序的定义,这种方法只能求出来LIS的长度,不能用于求解LIS本身。
- 使用一个队列,每轮不断优化其元素,使每个元素达到尽可能小(给后续递增留出最大的空间),其时间复杂度可达
O
(
n
log
n
)
O(n\log{n})
O(nlogn) (需要用到二分来查找插入位置,
2021/5/29
- 求数组中第K大的数字,可以使用基于快速排序的选择算法,降低时间复杂度
- 快速排序中,每一轮确定一个元素的最终位置,即它是第几大元素(从大到小)
- 如果该元素的下标是我所求的第K大,则直接返回该元素
- 否则根据情况向左、向右分治
2021/5/26
- 将无穷大这个值设置为
0x3f3f3f3f
的好处和1e9+7
一样,量级在10^9
,一般数字都小于它,可以用于无穷大。并且两个0x3f3f3f3f
相加,不会超出4字节32位int的表示范围。此外,用memset
进行初始化的时候,可以直接写成memset(arr,0x3f,sizeof arr)
,非常方便 - 唯一分解定理,算数基本定理:一个正数,如果是合数,则可以唯一分解为多个质数的乘积
- 注意在做方格题目的时候,如果是边7*7的矩形,则边上的点是8*8的
2021/5/25
- 再次反省,做题一定要考虑两件事
- 数据范围(long long,时间复杂度)
- 特殊情况(边界条件)
- dijkstra算法和floyd算法的区别是
- floyd代码简单,时间复杂度O(N^3)
- dijkstra代码复杂,时间复杂度低
- kruskal算法和prim算法的区别
- prim基于节点
- 适用于点少,边多的稠密图
- kruskal算法基于边
- 适用于点多,边少的稀疏图
- prim基于节点
2021/5/24
- 今天写蓝桥杯国赛
搭积木
这一题- 发现递归中,如果把一切问题都丢给下一层去解决,会造成栈溢出(虽然下一层第一步就返回了记忆化的结果)
- 将记忆化存储中已经有的答案直接返回, 减少不必要的递归,似乎能够很好地提高效率
- 但是平时写递归的时候,还是建议本层只处理本层的问题。
- 存在大量相同子问题的问题
- 动态规划
- 记忆化搜索
- 记忆化递归(也可以理解为一种搜索)
不是所有都适合动态规划,比如蓝桥杯国赛搭积木这一题,记忆化递归是最优方案
- 蓝桥杯遇到不会的题就想
- 贪心
- 模拟
- 搜索
- dp
- 状态压缩
- 二分、分治
- 图论
- 数论:gcd,最小公倍数,余数,差分,前缀和
- 全排列
- 什么时候用BFS,什么时候用迪杰斯特拉\弗洛伊德?
- BFS:当任意两节点之间的距离是某些特定值,如蓝桥杯国赛调手表那题,只能是1和k。
- 距离不固定:
- 迪杰斯特拉:复杂度低,O(N)
- 佛洛依德:复杂度高,三重循环O(N^3)
- 算法思想:如果a到c+c到b,比a到b近,则用c这条路替换
- 遇到题目,先想常规方法(如最优解,通常用BFS),再想特殊方法(如前缀和,dp,状态压缩,数学)
- 常规方法中,可以先想暴力法,再进行复杂度的优化。
2021/5/22
- 取余同余、前缀和、差分,这些都是重要的解题手段
- 动态规划其降低复杂度的原因在于,减少重复计算
- 所以如果写出来的dp转移方程,
dp[i]
需要与前i-1
项分别发生作用,那么就失去了动态规划的作用
- 所以如果写出来的dp转移方程,
2021/5/21
- 再一次发现逆向思维的重要性
- 在平方数那个问题中,如果直接遍历1、2、3…,然后判断i是否是平方数,则复杂度是O(n2),但是如果直接遍历平方数,即12,22,32…,则时间复杂度为O(n)
- 在贴瓷砖那题中,因为每个瓷砖是两个方块,如果枚举墙面每块的所有可能性再进行check,会发现非常难以进行检查。但是如果贴瓷砖的时候就按照两个两个进行贴,那么问题就非常简单了!
- 人的思维是很容易有漏洞的!不要想当然,多写写画画!
- 在做蓝桥杯国赛贴瓷砖那题,我理所当然认为,既然是从左上往右下贴,那么当前位置的下方和右方一定没有被访问过,但其实是有可能被访问过的,画个图就发现了。
- 对于二维数组,要想直接用指针+相对位置来访问某个元素
*(*arr+100)
这样才行- 因为
arr
自身是二维指针
2021/5/20
- 不要把邻接表的含义搞错了!它仅能表示一个节点和哪些节点相连,其链式结构不能等同于图中的先后顺序!
- 邻接表中要删除一条边,不能直接将一条链中某个节点的next置为NULL,会导致该链上后续的节点也被遗弃!
2021/5/19
- 在图中查找回路,采用拓扑排序的思想
- 对于有向图,每轮消掉所有入度为0的节点,最后剩下的就是回路
- 对于无向图,每轮消掉度数为1的节点,最后剩下来的就是回路
- BFS中需要使用set记录的path,来防止自己走回头路
- 今天在实现邻接表的时候发现一个大坑:
- 将指针a的指向赋给指针b,则两者指向的内容相同(a和b的存储单元中存储的地址值相同)
- 这时候如果修改b的指向(修改b存储单元地址值)是不影响a的指向的!
- 比赛遇到不会的,就考虑最朴素的暴力
- 图的表示有两种方法
- 邻接表(在面对大规模数据的时候,节省存储空间)
- 邻接矩阵
2021/5/18
- 状态压缩可用的地方很多,不止是状态压缩dp
- 前缀和前缀和,遇事不决前缀和
- 逆向思维非常重要
- 状态压缩中的,二进制状态,别忘了是用int表示的,前面还有很多位!!
- 你以为的
11011
状态,其实是0000...0011011
共32位
- 你以为的
- 使用二进制串表示状态时,如果不方便进行状态调整,可以将0和1的意思反过来,也许就方便了。
2021/5/17
-
数据规模往往决定着该题用什么算法
-
注意,树状数组的下标,一定要从1开始,否则lowbit操作和树状数组的逻辑不匹配
-
一个非负数,取二进制最低位的1和后边的0,即lowbit操作
- lowbit(a)=a &(~a+1)=a & (-a)
- 因为计算机中数字是以补码形式存放,负数的补码就是原码(除符号位)按位取反然后加一
- 那么一个正数取相反数的操作就是
- 符号位取反
- (除符号位)按位取反,然后加一
- 这两步组合在一起就是,所有位按位取反,然后加一,故
~a +1
=-a
-
Docker
- docker的特点
- 互相隔离
- 快速装载和卸载
- 规格标准化
- docker和虚拟机的根本区别:容器技术只隔离应用程序的运行时环境但容器之间可以共享同一个操作系统,这里的运行时环境指的是程序运行依赖的各种库以及配置。
- docker的特点
-
蓝桥杯遇到题目不会写,想一下是否是考察以下知识点
- 差分、前缀和
- dp、状态压缩dp
- 并查集
- 贪心
- 搜索
- 二分
- 数论
-
减少时间复杂度的方法
- 减少循环
- 使用哈希
- 二分
- 提前保存结果并利用
2021/5/15
- 差分和前缀和是对立统一的存在。
- 学到了,自建博客,解决图片问题,用外链图床就行了!
2021/5/14
- 在二维空间中,向量外积的一个几何意义就是:|a×b|在数值上等于由向量a和向量b构成的平行四边形的面积
- S Δ = ∣ 1 2 ⋅ a ⃗ × b ⃗ ∣ = ∣ ( x 1 ⋅ y 2 − x 2 ⋅ y 1 ) ∣ S_\Delta=|\frac 1 2\cdot \vec{a}\times\vec{b}|=|(x_1\cdot{y_2}-x_2\cdot{y_1})| SΔ=∣21⋅a×b∣=∣(x1⋅y2−x2⋅y1)∣
- 已知三角形顶点坐标求面积
- 使用海伦公式
p=(a+b+c)/2; S=sqrt(p(p-a)(p-b)(p-c) )
- 使用海伦公式
- 对于一个(正方形)二维数组的下标来说
- 区分主对角线两边元素的方法是:
- 若看y-x大于0,则在主对角线以上
- 区分副对角线两边元素的方法是:
- 若x+y<行数-1,则在副对角线以上
- 区分主对角线两边元素的方法是:
- 并查集判环的方法是
- 加入当前边之前,两个顶点如果已经在一个集合中了,那么加入该边后一定形成环
2021/5/13
- 拓扑排序
- 使用BFS广度优先遍历实现比较好
- 将所有入度为0的(从未入过队的)节点加入队列
- 不断取出队列中的节点,放入结果集末尾,并将其直接子节点的入度减一
- 重复步骤1和步骤2,直到队列为空
- 其核心思想是,每次将入度为0的节点加入结果集尾部,并且将该节点的直接子节点的入度减一
- 注意:如果存在环,则无法将环中节点加入结果集
- 使用BFS广度优先遍历实现比较好
- 一个常见的BUG是,把变量含义弄错,尤其是
i
和arr[i]
- 解决二分图问题,使用染色法
- 广度优先搜索进行染色
- 使用
lower_bound
和upper_bound
找到一个结果时,要想得到其下标,只需要用迭代器减去arr.begin()
即可 lower_bound
和upper_bound
是基于二分的,也就意味着序列需要是从小到大排序好的- 排序是重要算法思想
- 数学技巧:如果c位于a和b之间,那么c到a的距离+c到b的距离,是一个固定值,为(b-a);
- 在一些累积性的问题中,前缀和是非常有效的解题思路。
- 写代码前
- 理清思路,并且优化思路
- 关注数据大小,关注边界条件
- 然后开始编码
- 蓄水池抽样算法(又叫水库采样算法)
- 适用于总数未知的随机抽样
- 在对一个链表(长度未知)进行随机采样的时候,遍历一次链表,在遍历到第 m 个节点时,有 1/m 的概率选择这个节点作为结果,并且覆盖掉之前的结果。
class Solution {
public:
/** @param head The linked list's head.
Note that the head is guaranteed to be not null, so it contains at least one node. */
ListNode*myList;
Solution(ListNode* head) {
myList=head;
srand((unsigned int)time(NULL));
}
/** Returns a random node's value. */
int getRandom() {
int ans=myList->val; //初始选中第一个节点
ListNode*node=myList->next;
int i=2;
while(node){
if(rand()%i==0){ //1/i的概率选中当前的节点
ans=node->val; //覆盖掉之前的选择
}
i++;
node=node->next;
}
return ans;
}
};
- 写算法时,首先关注数据大小和边界条件
- 战术上的勤奋并不能弥补战略上的懒惰
- 方向错了,怎么努力也没有用
- Fisher-Yates洗牌算法
- 原理:通过随机交换位置来实现随机打乱,有正向和反向两种写法
- 反向洗牌比较好记,反向,并且只和序号小于等于自己的交换
//反向洗牌
for (int i = n - 1; i >= 0; --i) {
swap(shuffled[i], shuffled[rand() % (i + 1)]);
}
//正向洗牌:
for (int i = 0; i < n; ++i) {
int pos = rand() % (n - i);
swap(shuffled[i], shuffled[i+pos]);
}
- C++的rand()随机数是伪随机数,每次打印rand()是打印特定种子下的小M多项式序列的每一项
- 如果随机种子不变,那么每次执行程序,打印出来的一组随机数都与之前一组相同
- 并且该序列的周期是65535
- 头文件
<cstdlib>
2021/5/12
- 找出int范围内3^x的最大值,x为整数
int findLargest3Power(){
int x=3;
while(1){
int next=3*x;
//发生溢出时返回x
if(next/3!=x){
return x;
}
x=next;
}
}
- 判断一个数能否被开方
- 使用逆向思维!
//判断a能是否能被开方
bool canBeExtract(int a){
int res=sqrt(a);
return a==b*b;
}
-
C++判断一个浮点数小数部分是否为0
- 使用C++的浮点数取模函数
fmod(double , double)
fmod(a,1)==0
- 使用C++的浮点数取模函数
-
今日教训:写题目一定要草稿纸勾勾画画,不要想当然。
-
今天遇到一道很有意思的题目
- 开始采用的是返回min(2的数量,5的数量),后来题解中说因子5的数量远远小于因子2的数量,所以答案就是5的数量。
- 开始采用的是返回min(2的数量,5的数量),后来题解中说因子5的数量远远小于因子2的数量,所以答案就是5的数量。
-
写算法三大注意事项
- 关注数据大小和数据量
- 估算算法时空复杂度
- 注意边界条件
-
算数基本定理:任何一个大于1的自然数 N,如果N不为质数,那么N可以唯一分解成有限个质数的乘积
- 也可以表述为:任何一个合数都可以分解为质数的乘积。(1既不是素数(质数),也不是合数)
- 这里的唯一指的是,分解结果不看顺序的话,如3,4,5和5,4,3是一样的分解。
-
质数的定义:在大于1的自然数中,除了1和它本身以外,不再有其他因数的自然数。(只能被1和自身整除的大于1的自然数)
-
自然数肯定包括0,否则应该叫正数才对。
-
拓展欧几里得算法
- 用于计算xa+yb=gcd(a,b); 根据定理此式必有解
- 使用的时候,预先定义变量x和y,最后结果在存在x和y中
int extGcd(int a, int b, int &x, int &y) {
int gcd = a;
if (!b) {
x = 1, y = 0;
} else {
gcd = extGcd(b, a % b, y, x);
y -= (a / b) * x;
}
return gcd;
}
2021/5/11
-
写一道算法题,开始写代码之前要做的事
- 理清思路,搭好框架
- 估计所用方法的时间空间复杂度,对于题目中数据量
- 关注题中的数据大小
- 关注边界条件
-
今天刷到洛谷上一道题,觉得很有启发性。这一题要记录两行的状态,才比较方便解决问题
-
逻辑运算中,今天遇到一个大坑
- 一道状态压缩题目,炮兵攻击距离是3,也就是三排之间不能在一列
- 这时候判断三排的所有士兵不能在一列
- 正确写法
return !(st1 & st2) && !(st1 & st3) && !(st2 & st3)
- 错误写法
return !(st1 & st2 & st3);
- 正确写法
-
使用递归输出一个十进制数的二进制形式
void printBinary(int num) {
if(!num)
return;
printBinary(num>>1);
cout<<(num&1);
}
- C++
scanf
输入数字之后,如果要输入别的类型(如字符,字符串)一定要吸收换行符!(getchar()) - 做动态规划的题目,应该先理清思路,最好写出状态转移方程
- 如果输入的是一个二进制数如1101,如何存储为十进制?
- int t=0;每次读取一个字符c,然后
t<<=1;t+=c
;即可
- int t=0;每次读取一个字符c,然后
- 动态规划的问题要倒过来思考,正过来写代码
- 思想上是用后边的状态倒推前边的,但是写代码的时候要现有前边的状态才能求出后边的状态
2021/5/9
- 编程比赛中,该写
cstdio
头文件,还是要写,只有iostream
的话,printf
和scanf
有可能会报错 &
的优先级比+
低!!- 使用与或非的时候,一定要加括号!
- 取INF时用
1e9+7
是不错的选择- 两个数相加不爆int
- 两个数相乘不爆long long
- 对于TSP问题中需要回到出发点的问题,不要在状压dp中加以考虑,全部状态计算完之后,再单独处理,加上最后访问点到原点的距离即可。
- 计算最短路径的两种经典算法(图片截图自B站up主WAY_zhong的讲解视频)
-
迪杰斯特拉Dijkstra算法基于贪心
- 贪心:计算出每个节点到原点的最短距离,直到计算到des节点
-
佛洛依德Floyd算法基于动态规划
- 核心思想:如果f(a,c)+f(c,d)<f(a,d) 则更新f(a,d)=f(a,c)+f(c,d),如果需要路径的话将d的前驱记录为c即可
- 右边的二维表记录(有向图)某个节点最短路径上前一个节点。如果只需要计算最短路径长度,则不需要右边的二维表。
- 代码十分简洁,三重循环,选出src、des、mid节点即可。
-
有向图floyd
void floyd() {
for (int i = 0; i < N; i++) //src
for (int j = 0; j < N; j++) //des
for (int k = 0; k < N; k++) { //mid
if (i == j || i == k || j == k)
continue;
if (dist[i][j] > dist[i][k] + dist[k][j])
dist[i][j] = dist[i][k] + dist[k][j];
}
}
无向图floyd
for(int i=0;i<n;i++)
for(int j=i+1;j<n;j++)
for(int k=0;k<n;k++) //基于无向图的优化
if(dist[i][k]+dist[k][j]<dist[i][j]){
dist[i][j]=dist[i][k]+dist[k][j];
dist[j][i]=dist[i][j];
}
2021/5/8
- 处理股票买卖的动态规划问题时,有时建立
hold持有
和unhold不持有
两个dp数组会比buy
和sell
更好,因为对一支股票每天只有两种状态,持有或者不持有。 - 遇到复杂问题的编程,要把过程想清楚(最好画图),用注释搭好框架再写,如下边这题
class Solution {
public:
int findTargetSumWays(vector<int>& nums, int target) {
int arr[2001];
fill(arr,arr+2001,0);
arr[nums[0]+1000]++;
arr[-nums[0]+1000]++;
// 记录已经能表示的target
for(int i=1;i<nums.size();i++){
int N=nums[i];
int temp[2001];
fill(temp,temp+2001,0);
for(int j=0;j<2001;j++){
if(arr[j]>0){
temp[j-N]+=arr[j];
temp[j+N]+=arr[j];
}
}
memcpy(arr,temp,2001*4);
}
return arr[target+1000];
}
};
- 对一些在顺序上左右为难的问题,不妨先进行排序!
- 动态规划注意事项
- 注意人工增加项的初始化(尤其是多维dp的第一行第一列)
- 注意dp含义的设定与题目求解问题的关系(直接还是间接)
- 处理环形数组的办法是:丢弃第一个或者丢弃最后一个,最后取两种方案较优的那种
- 多个条件的动态规划,就像切几根香肠,把每一根(每个条件)都切成一片一片,然后来解决问题。
2021/5/7
- 今天花了一个下午做一道hard动态规划题目,最大的感触是,一定要定义好dp的含义,从含义出发,找
dp[i][j]
和之前的dp的关系。其次就是,多状态问题(如股票买卖),最好将多种状态分开dp。同时,总结结果时也从必须根据dp的定义来返回。
buy[i][j]
表示,0~i
天进行一共进行j
次买入,能够获取的最大利润
sell[i][j]
表示,0~i
天进行一共j
次卖出,能够获取的最大利润
这里的一共j次的一共非常重要,意味着第i天可以进行买\卖,也可以不买\不卖
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int d=prices.size();
//buy[i][j]表示,0~i天进行一共进行j次买入,能够获取的最大利润
vector<vector<int> >buy(d+1,vector<int>(k+1,INT_MIN));
//sell[i][j]表示,0~i天进行一共j次卖出,能够获取的最大利润
vector<vector<int> >sell(d+1,vector<int>(k+1,0));
for(int i=1;i<d+1;i++){
int price=prices[i-1];
for(int j=1;j<=k;j++){
//两种情况,在第i天买入股票,或者不买
buy[i][j]=max(buy[i-1][j],sell[i-1][j-1]-price);
//两种情况,在第i天卖出股票,或者不卖
sell[i][j]=max(sell[i-1][j],buy[i][j]+price);
}
}
return sell[d][k];
}
};
dp一般有两种描述
- 一种是跨度,从0到i项一共xxx
- 一种是标度,第i项xxx
2021/5/6
- 当有多个变化的条件时,从一个条件的变化出发,比较容易有思路
- 当没有低复杂度的算法时,从高复杂度的算法出发,然后进行优化
- 记录一道很棒的动态规划的题目,我对
*
的处理值得回味
class Solution {
public:
bool isMatch(string s, string p) {
int len1 = p.length(), len2 = s.length();
vector<vector<bool> > dp(len1 + 1, vector<bool>(len2 + 1, false));
dp[0][0] = true;
for (int i = 1; i < len1 + 1; i++)
for (int j = 0; j < len2 + 1; j++) {
//第一列特殊处理
if (j == 0) {
dp[i][j] = (p[i - 1] == '*' && dp[i - 2][j]);
continue;
}
//p串末尾是*
if (p[i - 1] == '*') {
//情形1:*表示0个前边的元素
//清醒2:*表示n(n>0)个前边的元素
dp[i][j] = dp[i - 2][j] || ((p[i - 2] == s[j - 1] || p[i - 2] == '.') && dp[i][j - 1]);
continue;
}
//p串末尾不是*
dp[i][j] = (p[i - 1] == s[j - 1] || p[i - 1] == '.') && dp[i - 1][j - 1];
}
return dp[len1][len2];
}
};
2021/5/5
- leetcode这一题太经典了,记录下来
class Solution {
public:
//统计字符串中0和1的个数
void count(string &str,int &count0,int &count1){
for(int i=0;i<str.length();i++){
if(str[i]=='0')
count0++;
else
count1++;
}
}
int findMaxForm(vector<string>& strs, int m, int n) {
int len=strs.size();
//dp[i][j]表示,有i个0和j个1的情况下最多容纳几个子集
vector<vector<int> >dp(m+1,vector<int>(n+1,0));
//每个字符串是压缩空间前的横坐标
for(int k=0;k<strs.size();k++){
string &str=strs[k];
int count0=0,count1=0;
count(str,count0,count1);
//01背包,后向遍历,防止重复使用本轮的字符串
for(int i=m;i>=count0;i--){ //i<count0,无法容纳该字符串,则dp[i][j]维持上一轮的数据
for(int j=n;j>=count1;j--){
//分两种情况,一种选中这个字符串,一种不选中这个字符串
dp[i][j]=max(dp[i][j],1+dp[i-count0][j-count1]);//第一个dp[i][j]是上一轮的结果
}
}
}
return dp[m][n];
}
};
- 0-1背包多为费用问题和一维费用问题本质是相同的,即通过假设占用部分(多维)容量,由于剩下的(多维)容量以前已经处理过了,所以可以进行动态规划。
- 0-1背包问题要点如下
- 不压缩空间:横坐标是要装的物品,纵坐标是背包容量
- 选中当前物品,则假设占用部分容量,看剩下部分容量之前如何处置的
- 不选中当前物品
- 压缩空间:后向遍历,以避免本轮物品的重复取用
- 不压缩空间:横坐标是要装的物品,纵坐标是背包容量
- 对于多费用的01背包问题,其解题过程和普通的01背包问题相同(强烈建议使用空间压缩,注意后向遍历),其动态规划形式似下图:
普通01背包
普通01背包压缩空间
多维费用01背包
多维费用01背包压缩空间后
- 自认为我分析得很精妙的一题
- 我的思考,既然要求最长递增子序列,那么我就维护一个最长递增子序列,nums中从左到右每个元素都是用来优化这个维护着的序列的。
- 如何优化?答:尽可能地使维护序列中的每个元素最小。这样就为序列的增长提供了最充足的空间!
使用lower_bound解决问题!(找到第一个大于等于key的数,返回其迭代器)
class Solution {
public:
int lengthOfLIS(vector<int>& nums) {
vector<int>res;
for(int i=0;i<nums.size();i++){
int key=nums[i];
if(res.empty()){
res.push_back(key);
continue;
}
if(key>res[res.size()-1])
res.push_back(key);
else{
*lower_bound(res.begin(),res.end(),key)=key;
}
}
return res.size();
}
};
- 使用dp数组时,有时dp下标和所求事物的下标有差异(主要是0作首位还是1作首位),这时候重新定一个变量index记录所求事物下标,会让思路更清晰,如下:
class Solution {
public:
int numDecodings(string s) {
//dp[i]表示:0~i位置一共有多少种编码方法
int len=s.length();
vector<int>dp(len+1,0); //s的下标对应dp的1、2、3...
//首个是0就无解
if(s[0]=='0')
return 0;
//排除长度为1的情况
if(len==1)
return 1;
//开始dp
dp[0]=1,dp[1]=1;
stringstream ss;
int t;
//i是dp的下标
for(int i=2;i<len+1;i++){
//s的下标
int index=i-1;
//独立算
if(s[index]!='0')
dp[i]+=dp[i-1];
//和前边合并算
if(s[index-1]!='0'){
ss<<s.substr(index-1,2);
ss>>t;
ss.clear();
if(t>=10 && t<=26)
dp[i]+=dp[i-2];
}
if(dp[i]==0)
break;
}
return dp[len];
}
};
2021/5/4
-
写代码时,养成习惯:先理清思路,接着搭好框架(注释),最后再编码
-
在使用动态规划时,可以进一步思考是否可以空间优化。
-
重新写爬楼梯
leetcode70
这一题,想都没想直接用递归,结果测试用例给了个44,超出时间限制。- 我的思维存在的漏洞:在有一个思路之后,并没有考虑时间复杂度和问题规模。
- 这一题如果使用递归的话,其复杂度是
2^n
,1s最多能容纳的n的规模约为27。 - 1s不同复杂度能处理的规模
- n : 108
- n^2 : 104
- n^3 : 500以下
- 2^n : 27以下
- logn : 2^(108) ⭐
- 所以这一题应该使用动态规划求解,避免重复计算子问题的答案