PS:关于大根堆,小根堆的或者说大堆顶和小堆顶的知识,可以看这篇博客,讲的很详细。可以用下面的例子来总结。简单地说就是大顶堆是倒三角出队列,默认大顶堆,对应两种形式他是less的。
#include<iostream>
#include <queue>
using namespace std;
int main()
{
//对于基础类型 默认是大顶堆
priority_queue<int> a;
//等同于 priority_queue<int, vector<int>, less<int> > a;
priority_queue<int, vector<int>, greater<int> > c; //这样就是小顶堆
priority_queue<string> b;
for (int i = 0; i < 5; i++)
{
a.push(i);
c.push(i);
}
while (!a.empty())
{
cout << a.top() << ' ';
a.pop();
}
cout << endl;
while (!c.empty())
{
cout << c.top() << ' ';
c.pop();
}
cout << endl;
b.push("abc");
b.push("abcd");
b.push("cbd");
while (!b.empty())
{
cout << b.top() << ' ';
b.pop();
}
cout << endl;
return 0;
}
输出:
4 3 2 1 0
0 1 2 3 4
cbd abcd abc
题目:
有一堆石头,用整数数组 stones 表示。其中 stones[i] 表示第 i 块石头的重量。
每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;
如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0。
示例1:
输入:stones = [2,7,4,1,8,1]
输出:1
解释:
组合 2 和 4,得到 2,所以数组转化为 [2,7,1,8,1],
组合 7 和 8,得到 1,所以数组转化为 [2,1,1,1],
组合 2 和 1,得到 1,所以数组转化为 [1,1,1],
组合 1 和 1,得到 0,所以数组转化为 [1],这就是最优值。
思路:
这个题目有点像昨天的每日一题:目标和,看题目似乎挺复杂,每次都是挑选两块进行消融,说白了其实就是求数组相互加减后得到的最终非负值最小解。在这里可以借助深搜,时间复杂度为o(2^n),而题目stones的长度最大可以到30,这个时间复杂度让程序在最后是十几个测试样例中超时了~ 因此必须对算法进行剪枝和优化~ 下面现附上我的超时代码:
class Solution {
vector<int> res;
public:
int lastStoneWeightII(vector<int>& stones) {
dfs(stones,0,0);
sort(res.begin(),res.end());
return res[0];
}
//dfs程序
void dfs(vector<int>& stones,int sum, int index){
if(index==stones.size()){
if(sum<0)
return;
else{
res.push_back(sum);
return;
}
}
dfs(stones,sum+stones[index],index+1); //有什么办法可以剪枝呢。2的32次方的话就太大了
dfs(stones,sum-stones[index],index+1);
}
};
思路2:
为了优化结构,看来不能用dfs的思路,因此我们可以尝试使用动态规划来求解。和背包问题类似。我们可以吧数组分为两个组,一个是加法组,一个是减法组。两者相差的绝对值最小值就是我们需要的最小差值。同时注意到,我们需要保证!保证这个差值是大于0的,也就是说负数组累加最大不超过总和的一半。这个是个约束条件。
程序中就是每次都不断变化可以参与的石头个数,计算这么多石头的情况下能依次遍历背包的容量。这样做的好处是,对每一块石头,除了达到完全超过任意一个最大背包的石头,在sum/2的范围内,因为是在遍历,都能找到它的容身之所,比如dp[i][stone某个],而且注意到true一旦确定就是不会变成false了!!!!这很关键,所以可以索引上一个来确定下一个是否是true。因为2个石头能装满j的,我3个石头完全可以选择不装满,或者看上一个也就是最新的情况是否装满~~~!!或者上一个最新情况中是否装满了j-当前石头装量情况。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
int sum=accumulate(stones.begin(),stones.end(),0);
int m=sum/2,n=stones.size();
vector<vector<int>>dp(n+1,vector<int>(m+1));
dp[0][0]=true;
for(int i=0;i<n;i++){
for(int j=0;j<=m;j++){
if(j<stones[i])
dp[i+1][j]=dp[i][j]; //就是这个石头数量下能不能装到j的数量,不能装!那能不能就是取决于上一块石头就这块太大了,都超过最大空间了,那我不要,但是不要的情况下能否装上取决于上一块石头能否装
else{
dp[i+1][j]=dp[i][j-stones[i]] || dp[i][j];//石头还好没超过,装不装看上一块满了么或者上一块是不是正好差这么多。
}
}
}
for (int j = m; ; --j) {
if (dp[n][j]) {
return sum - 2 * j;
}
}
}
};
优化下,上面的思路不符合01背包,我们这么想,和416分割子集很像,这种问题中,背包的容量是石头重量的一半,简单来说就是任意选i块石头,使得他们的重量趋近于总重量的一半,因为这样和另一半抵消的差值就是最小的,这个思路是没问题的,两堆石头互项怼,左边的要么比你小,那后面的人补,要么比你大,我继续消没问题的。那么背包问题首先要确定j就是背包的容量,为石头重量的一半,然后i就是石头的个数,01背包就是选和不选,然后只能选1次,我们要让选择的石头重量尽可能趋向于总重量一半,这个基准从原来的价值最大到一个固定值,因此我们让石头的重量就等于价值,要让石头的重量或者说价值等于背包的容量! dp[j]变成容量为j的背包,最大可以背j的这么重的石头,每个石头的价值等于他的重量。我们让价值最大就是容量j的背包背的石头重量尽可能接近容量。或者说这种价值和重量相同的设置让j背包容量最大的容纳价值是j,我们要让dp[j]尽可能接近j,那么最终就是求sum-dp[j]与dp[j]的差值。
class Solution {
public:
int lastStoneWeightII(vector<int>& stones) {
//简单来说就是任意选i块石头,使得他们的重量趋近于总重量的一半,因为这样和另一半抵消的差值就是最小的
//两堆石头互项怼,左边的要么比你小,那后面的人补,要么比你大,我继续消没问题的。
vector<int> dp(15001, 0);
int sum = 0;
for (int i = 0; i < stones.size(); i++)
sum += stones[i];
int target = sum / 2;
for (int i = 0; i < stones.size(); i++) { // 遍历物品
for (int j = target; j >= stones[i]; j--) // 遍历背包
dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);
}
return sum - dp[target] - dp[target];
}
};
————————————————————————————————————————
题目2: 最后一块石头的重量I
有一堆石头,每块石头的重量都是正整数。每一回合,从中选出两块 最重的 石头,然后将它们一起粉碎。假设石头的重量分别为 x 和 y,且 x <= y。那么粉碎的可能结果如下:
如果 x == y,那么两块石头都会被完全粉碎;如果 x != y,那么重量为 x 的石头将会完全粉碎,而重量为 y 的石头新重量为 y-x。最后,最多只会剩下一块石头。返回此石头的重量。如果没有石头剩下,就返回 0。
思路1:
这个题目和上面题目的区别在于我们每次拿的是最大的两块石头,求最后剩下的情况。最直接想到的是排序然后处理,再排序再处理。数组长度是n,每次处理肯定消掉至少1个石头,那我最多处理n-1次,就只会剩下一个石头了。关键在于怎么处理。很多时候想到思路就是不愿意去继续想了,所以写不出来。我们要大但假设。别像乱七八糟的。我们每次排序,然后从最大值index开始减去index-1下标处对应的元素,然后让index-1对应的元素是0,index对应的元素是两者相差,然后继续排序。注意到每次最大值的索引都在index处,数组元素不变,只是0在变多。但是为了优化速度,增加一个break条件,因为有时候是2个元素完全相同的消除,一下子少了2个元素。
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
int index = stones.size() - 1;
for (int i = 1; i <= stones.size() - 1; i++){ //最多比较length-1次
sort(stones.begin(),stones.end());
if (stones[index - 1] == 0) break; //说明最多只剩一块石头这个处理很多时候一些元素相同一次性消除两个就不用比这么多次了
stones[index] -= stones[index - 1]; //两种不同情况使用同一表达式处理
stones[index - 1] = 0; //每次比较后肯定会有一块石头被destroyed
}
return stones[index];
}
};
思路2:
思路2中用到了优先队列的知识,和思路1类似,只不过排序和处理完全可以交给优先队列来处理了。注意到简单的队列定义中默认是大顶堆,就是队头最大,后面减小。大顶堆就是从大到小出,小顶堆就是从小到大出。我们先将元素入大顶堆,每次取出两个进行差值,等于0就不入队列了,不等于0就入队列,直到队列元素小于等于1,就退出循环~
class Solution {
public:
int lastStoneWeight(vector<int>& stones) {
priority_queue<int> que;
for(int i=0;i<stones.size();i++){
que.push(stones[i]);
}
while(que.size()>1){
int temp=que.top();
que.pop();
temp=temp-que.top();
que.pop();
if(temp>0)
que.push(temp);
}
return que.empty() ? 0:que.top();
}
};