递归
题目解析
https://leetcode.cn/problems/sum-of-all-subset-xor-totals/
算法原理与性能分析
这个和之前的第一道算法题一样,决策树都是看i位置的元素是否选择来画的,最后得到所有的vector集合,最后再求每一个vector中元素的异或和就好。
找子集的时间复杂度是O(2n),然后求异或的时间复杂度是O(2n * n),最后的时间复杂度为O(2n(n+1)),可以看到的是复杂度主要还是在求异或上面,因为有2n个数组,每个数组都要遍历一遍,所以优化主要还是在这一方面,想到的是在dfs的时候就直接把异或算出来,就不用最后再遍历一遍2n结点了,时间复杂度可以优化到???怎么感觉没优化呀,在叶子结点也是要O(N)遍历的。2n-1 + 2^n*n大概是这样这感觉完全没优化啊???没招了。只能问AI了
优化后的思路:
- DFS 递归时,除了传递
<font style="color:rgb(15, 17, 21);">path</font>(可省略),还传递当前子集的异或值<font style="color:rgb(15, 17, 21);">curXor</font> - 选一个元素时:
<font style="color:rgb(15, 17, 21);">curXor ^= nums[pos]</font> - 到叶子结点时,
<font style="color:rgb(15, 17, 21);">curXor</font>就是这个子集的异或值,直接加到结果中
就是说我之前考虑的还是在最后一层,这里是通过一个空间直接在dfs的时候帮我们计算,很漂亮的做法。
还有其他算法吗为什么选择这个算法?
在之前那题求子集的题目中我们可以根据选择元素的个数来优化,但是时间复杂度实际上只是优化了一个常数,感觉提升不高,因此这里不做选择,从代码可读性来说还是这个算法要高一点,因此这里我们选择可读性而不是一味的提高算法的性能而且还是有限的情况下。
代码编写
cpp
class Solution {
vector<vector<int>> ans;
private:
void dfs(vector<int>& nums,vector<int>& path,int pos)
{
if(pos == nums.size())
{
ans.push_back(path);
return;
}
//不选
dfs(nums,path,pos+1);
//选
path.push_back(nums[pos]);
dfs(nums,path,pos+1);
//回溯
path.pop_back();
}
int calculate(vector<int>& v)
{
if(v.size() == 0) return 0;
int ret = v[0];
for(int i = 1;i < v.size();i++)
{
ret ^= v[i];
}
return ret;
}
public:
int subsetXORSum(vector<int>& nums) {
//10 101 110 //001
//本质还是把所有的子集(实际上是全排列)找到,然后每个排列算一下异或,最后加起来
//和第一题是有点像的,还是先画决策树,然后根据决策树来写代码
int n = nums.size();
vector<int> path;
int pos = 0;
dfs(nums,path,pos);
int ret = 0;
for(int i = 0;i < ans.size();i++)
{
ret += calculate(ans[i]); //这个函数返回vector中所有元素的异或值
}
return ret;
}
};
优化为O(2^N)的代码:
cpp
class Solution {
int total = 0;
private:
void dfs(vector<int>& nums,int pos,int curXor)
{
if(pos == nums.size())
{
total += curXor;
return;
}
//不选
dfs(nums,pos+1,curXor);
//选
dfs(nums,pos+1,curXor ^ nums[pos]);
//回溯,因为是拷贝因此dfs自动帮我们回溯
}
public:
int subsetXORSum(vector<int>& nums) {
//10 101 110 //001
//本质还是把所有的子集(实际上是全排列)找到,然后每个排列算一下异或,最后加起来
//和第一题是有点像的,还是先画决策树,然后根据决策树来写代码
int n = nums.size();
dfs(nums,0,0);
return total;
}
};
这样就能干掉100%了,在n<=12的情况下。
动态规划
题目链接
https://leetcode.cn/problems/delete-and-earn/
算法原理与性能分析
因为要拿到nums[i]-1和nums[i]+1的值,因此我想到的是哈希表,因为数组的中间删除和查找太慢,想到的是把所有的数再拷贝一份到哈希表中,存储这个数字和它的一个数量,当我选择一个数的时候,对应的数量就自减,然后我不是要清除掉这个nums[i]-1和nums[i]+1嘛,也可以通过哈希表找到然后让数量为0,很方便。
但是问题是什么呢,就是我们知道动态规划是一般是线性的,无论是一维还是二维,但是我们用哈希表之后
状态表示该怎么写呢?状态表示写不出来,转移方程也就写不出来了。
因此我想的还是不能依靠哈希表,最好通过双指针来解决查找和删除的问题。首先我们需要给数组升序排序,然后如果我们要拿到一个积分的话,我们用left和right指针分别拿到这个左边的不同值,和右边的不同值,说实话感觉还是不好写...,我想用vis数组表示i位置是否可以用,但是总感觉差点火候~,还是只能问ai了哈。
这道题的标准解法,是一个惊人的思维转换 :它本质上就是"打家劫舍"问题。
具体转换步骤:
- 统计总和 :创建一个新数组(或哈希表)
<font style="color:rgb(15, 17, 21);">sum</font>,其中<font style="color:rgb(15, 17, 21);">sum[x]</font>代表所有值为 x 的数字之和 。- 比如
<font style="color:rgb(15, 17, 21);">nums = [2,2,3,3,3,4]</font>,转换后:<font style="color:rgb(15, 17, 21);">sum[2] = 4</font>(两个2的和)<font style="color:rgb(15, 17, 21);">sum[3] = 9</font>(三个3的和)<font style="color:rgb(15, 17, 21);">sum[4] = 4</font>(一个4的和)- 其他没出现的数字(如0,1,5...)
<font style="color:rgb(15, 17, 21);">sum[i] = 0</font>
- 比如
- 建立约束 :原问题中"选了
<font style="color:rgb(15, 17, 21);">x</font>就不能选<font style="color:rgb(15, 17, 21);">x-1</font>和<font style="color:rgb(15, 17, 21);">x+1</font>",在新数组里就变成了 "选了******<font style="color:rgb(15, 17, 21);">sum[i]</font>**就不能选相邻的**<font style="color:rgb(15, 17, 21);">sum[i-1]</font>**和**<font style="color:rgb(15, 17, 21);">sum[i+1]</font>**"。 - 套用"打家劫舍" :现在问题完美转化为:在一个数组(下标从
<font style="color:rgb(15, 17, 21);">minNum</font>到<font style="color:rgb(15, 17, 21);">maxNum</font>)中,不能取相邻元素,求能取到的最大值。这正是"打家劫舍"问题。
这样一来状态表示就很好写了:
dp[i] 表示选取到i位置能获取的最大值。
状态转移方程:dp[i] = max(dp[i-1],dp[i-2]+sum[i]);
初始化:dp[0] = 0,dp[1] = sum[1];
填表顺序就是从左向右天就行,因此以后面对相邻这些问题要有一定的敏感度~
其他算法与选择策略?
动态规划这里感觉算法模版都是比较固定的,说实话就算要提升性能也不过是常数级别的,大方向很难有提高。能想到动态规划就已经不错了,后面在练算法竞赛方便才会需要进一步提升性能~
打家劫舍问题~
代码编写
cpp
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
//状态表示感觉不好想啊
//
vector<int> sum(10001);
for(auto& i : nums)
{
sum[i] += i;
}
vector<int> dp(10001);
dp[1] = sum[1];
for(int i = 2;i < 10001;i++)
{
dp[i] = std::max(dp[i-1],dp[i-2]+sum[i]);
}
return dp[10000];
}
};
贪心
题目解析
https://leetcode.cn/problems/wiggle-subsequence/
算法原理与性能分析
题目等价于给一个数组,如何在最小的删除次数下,使得它成为一个摆动序列这样子~
其实这里我是有点先入为主的感觉,因为这题是放在贪心算法下面的,因此我一上来就想的是怎么贪心,但是实际上这是不合理的,因为我在做题的时候一上来并不知道用什么算法,或许我应该考虑动态规划,或者是双指针这样子~
我的刚开始的一个思路就是不断的从数组中找到不满足摆动序列的数,比如我这个数和前面一个数相同,那我应该删除一个,或者说有三个数,这三个数是依次递减的,此时就有一个贪心了,为了让后面即使比较小的数也能呈现一个上升的状态,因此我这里应该选一个最小的数,让较大的数弹出;三个依次递增的数也是一样的,为了能让后面比较大的数也能呈现一个递减的状态我这里应该选一个最大的数,,让较小的数弹出。
但是在写代码遇到的问题是什么呢?因为我们需要频繁地弹出,因此nums的size会变化,不能在一个for循环里面遍历所有位置,一旦找到不合理的位置循环就要停止等待下一次找。还有一个问题就是需要很多次的判断这个数组是否是摆动序列,每次判断都是O(N),这个是性能难以提高的两个问题。还有就是边界的处理也显地很冗余麻烦,并不算可读性强~
其他算法与选择?
因此我们不应该改变数组状态,而是统计数组状态~,就是一个是查一个是改。实际上我们只需要直接统计波峰和波谷即可。用一个prediff记录之前的状态是上升还是下降或者开始,如果现在的状态和之前不一样就统计,否则继续遍历。可以看到的是代码也很简洁,很完美。
代码编写
cpp
class Solution {
int n;
private:
bool iswiggleList(vector<int>& nums)
{
if(nums.size()==1) return true;
else if(nums.size()==2) return nums[0] != nums[1];
bool flag = true; //正
for(int i = 1;i < nums.size();i++)
{
if(flag == true)
{
if(nums[i]-nums[i-1]>0) continue;
else return false;
}
else
{
if(nums[i]-nums[i-1]<0) continue;
else return false;
}
}
return false;
}
public:
int wiggleMaxLength(vector<int>& nums) {
//怎么贪?
//原来是增,现在要减但是还是增,需要保留最大的那个,中间的增删掉
//原来是减,现在要增但是还是减,需要保留最小的那个,中间的删除掉
//如果是相等的直接随便删一个就行
n = nums.size();
while(!iswiggleList(nums))
{
//从头向尾部扫描
for(int i = 1;i < nums.size();i++)
{
if(nums[i]==nums[i-1])
nums.erase(nums.begin()+i);
}
if(iswiggleList(nums)) break;
for(int i = 1;i < nums.size()-1;i++)
{
if(nums[i]-nums[i-1]>0 && nums[i+1]-nums[i]>0)
{
nums.erase(nums.begin()+i);
}
}
if(iswiggleList(nums)) break;
for(int i = 0;i < nums.size()-1;i++)
{
if(nums[i]-nums[i-1]<0 && nums[i+1]-nums[i]<0)
{
nums.erase(nums.begin()+i);
}
}
if(iswiggleList(nums)) break;
}
}
return nums.size();
};
cpp
class Solution {
public:
int wiggleMaxLength(vector<int>& nums) {
int n = nums.size();
if(n < 2) return n;
int count = 1;
int preDiff = 0; //开始为0,>0上身, <0 下降
for(int i = 1;i < n;i++)
{
int currDiff = nums[i] - nums[i-1];
if(currDiff>0 && preDiff<=0 ||
currDiff<0 && preDiff>=0)
{
count++;
preDiff = currDiff;
}
}
return count;
}
};