前几天阳了,同时纠结了一下到底搞开发,算法还是读博,现在继续。
一、背包问题
1、为什么可以状态转移
因为每一步的决策数是有限的,回溯也类似。
2、为什么不使用回溯而是用状态转移
因为回溯相当于把所有可能性遍历了一遍,而状态转移一般要求给出某个最值,而求某个最值不需要遍历所有的情况。
使用背包解决问题的时候要确定好背包是什么,物体是什么。
3、什么是完全背包
01背包使用一维数组的时候,为了防止重复加物品,所以是从后往前遍历的,而这里是从前往后遍历的,这样就可以把前面的结果累加,达到物品重复放的效果。
4、为什么01背包要物品先遍历,而完全背包无所谓先遍历物品还是背包?
01背包中背包容量采取的是倒序遍历,而且01背包最需要的数据其实来自左上角(如果是二维数组)所以如果背包在外层遍历,物品在内层遍历,那么左上角的数据还没有算出来的时候,就已经计算下一个物品状态了,这是不合理的。
完全背包从前向后遍历,每次都可以及时更新左上角的数据,所以没有关系。
纯完全背包是无所谓遍历嵌套先后顺序的,但是如果要求组合数和排列数,就有所谓了。
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
5、初始化如何确定?
主要看可不可以累加出来。一般求组合数排列数,就是要初始化为1。
二、leetcode*2
由组合和排序引出完全背包的两道题:
518. 零钱兑换 II
难度:中等
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5]
输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
示例 3:
输入: amount = 10, coins = [10]
输出: 1
注意,你可以假设:
0 <= amount (总金额) <= 5000
1 <= coin (硬币面额) <= 5000
硬币种类不超过 500 种
结果符合 32 位符号整数
这道题就是完全背包的组合体,就直接正序去做,背包容量在外遍历:
class Solution {
public:
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1);
dp[0] = 1;
for(int i = 0; i < coins.size(); i++){
for(int j = coins[i]; j <= amount; j++){
dp[j] += dp[j - coins[i]];
}
}
return dp[amount];
}
};
377. 组合总和 Ⅳ
难度:中等
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:
nums = [1, 2, 3] target = 4
所有可能的组合为: (1, 1, 1, 1) (1, 1, 2) (1, 2, 1) (1, 3) (2, 1, 1) (2, 2) (3, 1)
请注意,顺序不同的序列被视作不同的组合。
因此输出为 7。
这道题就是排列题,遍历顺序就是先遍历物品,再遍历背包。
class Solution {
public:
int combinationSum4(vector<int>& nums, int target) {
vector<int> dp(target+1);
dp[0] = 1;
for(int j = 0; j <= target; j++)
for(int i = 0; i < nums.size(); i++)
if(j >= nums[i] && dp[j] < INT_MAX - dp[j - nums[i]])
dp[j] += dp[j - nums[i]];
return dp[target];
}
};
但是要注意的是,由于排列数是可能超过INT_MAX,由于保证了答案是不超过最大范围的,所以可以先做一个条件判断。
三、effective c++
条款20 :宁以pass-by-reference-to-const替换pass-by-value
就是正常函数调用的时候,如果传值,那么就会调用拷贝构造函数(先构造父类成员,然后构造父类,构造子类成员,最后构造子类)这是很耗时的,所以可以用常引用(其实就相当于const T* const ptr)
bool validateStudent (const Student& s)
还有就是用virtual实现多态的时候,要使用指针和引用才能实现动态绑定,所以如果Windows类是父类,内部有一个虚函数display,子类为WindowWithScrollBars,如果直接使用传值,调用拷贝构造函数,是无法实现多态,实现的也只会是父类的函数:
void printNameAndDisplay (Window w)
如果使用指针或者引用的话,就可以实现多态:
void printNameAndDisplay (const Windows& w)
然后其实一些小的类型传值就可以了,因为传地址还要四个字节,有些内置类型比较大,如String,就可以考虑使用传引用。
条款21:必须返回对象时,别妄想返回其reference
就是说返回引用的话,会出现各种各样的问题。
以operator重载为例。
1、如果将临时对象赋给引用,那么对象离开作用域就被销毁了,指针指向栈内存也许一开始不会变化,但是随着代码的变化,栈就会变化,这是糟糕的。
2、如果new一个对象返回的话,没有合理的办法取得operator返回的那个指针,特别当这种情况:
w = x * y * z
两个对象几乎没办法获取。
3、返回一个static对象,
一个问题就是多线程的话,C++0x以后是保证安全的,但是之前的话应该就要上锁了。
另外一个问题就是如果出现(a*b) == (c * d),翻译过来就是:
operator==(operator*(a, b), operator*(c, d))
那么恒等于真,因为内部两个乘运算的时候,static值其实改变了。
4、返回static array更难管理了/
所以干脆就别返回引用了哈,直接传值就完事了,交给编译器公司去优化吧。
之前条款10返回引用的时候,返回的*this对象,是本来就存在的对象,所以不用考虑对象是否临时的问题,而且operator=不涉及到运算,自然也没有static的问题。