贪心算法
贪心算法有很多经典的应用:霍夫曼编码、prim和kruskal最小生成树算法、dijkstra单源最短路径算法。
贪心算法解决问题步骤
很常见的一个问题,比如背包问题。
- 第一步,当我们看到这类问题的时候,首先要联想到贪心算法:针对一组数据,我们定义了限制值和期望值,希望从中选出几个数据,在满足限制值的情况下,期望值最大。
- 第二步,我们尝试看下这个问题是否可以用贪心算法解决:每次选择当前情况下,对限制值同等贡献量的情况下,对期望值贡献最大的数据。
- 第三步,我们举几个例子看下贪心算法产生的结果是否是最优的。
贪心算法不工作的原因:主要需要考虑前面的选择是否会影响到后面的选择(局部最优并不能保证全局最优)
贪心算法实战:分糖果问题;钱币找零;区间覆盖;
上面的"区间覆盖"问题的关键点就是找出贪心算法模型。之所以“覆盖区间”问题比前面几个问题感觉难度大一些,是因为那个我们想尽量大(或小)的变量不容易一眼看出来。背包豆子是单价尽量大;分糖果是用尽量小的糖果优先满足需求小的孩子;找零钱是尽量用大的面额;区间覆盖需要想到让右边未覆盖的区间尽量大。寻找贪心算法模型虽然没有一个通用的方法,而且老师也说了需要多练习才能对贪心问题有感觉,但是我们还是可以总结出一些启发式方法,这里我总结两个:1.通过画图增加对问题的理解。2.寻找那些跟问题有直接或间接关系的"尽量大(或小)"的变量。
找零问题不能用贪婪算法,即使有面值为一元的币值也不行:考虑币值为100,99和1的币种,每种各一百张,找396元。
动态规划可求出四张99元,但贪心算法解出需三张一百和96张一元。
贪心算法和动态规划分别用在什么地方??
实例
1. 分发饼干
假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。
对每个孩子 i,都有一个胃口值 g[i],这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j,都有一个尺寸 s[j] 。如果 s[j] >= g[i],我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。
示例:
输入: g = [1,2,3], s = [1,1]
输出: 1
解释:
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
思路:需要先都进行排序,需要注意的点是,期望值和限制值都同时++,然后在比较g[i]和s[j]的情况,如果不满足g[i]>s[j],需要j++,找到第一个满足g[i]>=s[j]的情况。
public int findContentChildren(int[] g, int[] s) {
//将g和s进行排序
Arrays.sort(g);
Arrays.sort(s);
int result = 0;
for(int i =0,j=0;i<g.length&&j<s.length;i++,j++){
while(j<s.length&&g[i]>s[j]){
j++;
}
if(j<s.length){
result++;
}
}
return result;
}
2. 最长回文串
给定一个包含大写字母和小写字母的字符串,找到通过这些字母构造成的最长的回文串。
在构造过程中,请注意区分大小写。比如 “Aa” 不能当做一个回文字符串。
注意:
假设字符串的长度不会超过 1010。
示例 1:
输入:
“abccccdd”
输出:
7
解释:
我们可以构造的最长的回文串是"dccaccd", 它的长度是 7。
思路:先要统计每个字符出现的次数v,然后可以用来回文的数就是v/2*2;除此之外,如果出现了奇数个,只能用一次加入到v中,例如aba,其中b出现了一次可以加入,但是如果还有一个也出现了奇数词,就不可以了,比如abca,只能选用b和c中的一个。
public int longestPalindrome(String s) {
//先将每个字母出现的次数放入到响应的数组中去
int[] count = new int[128];
//将每个字符出现的次数放入到数组中去
for(int i=0;i<s.length();i++){
char c = s.charAt(i);
count[c]++;
}
int result = 0;
//遍历count数组,找出可以组成回文串的长度
for(int v :count){
result += v/2*2;
if(v%2==1&&result%2==0){
result++;
}
}
return result;
}
3. 验证回文字符串II
给定一个非空字符串 s,最多删除一个字符。判断是否能成为回文字符串。
示例 1:
输入: s = “aba”
输出: true
思路:这个题和上个题的不同点是,这个题的string的顺序不能变化,所以不能再上题目的基础上去做,最直接的思路就是,先判断原始串是否是回文,如果是就直接返回true,否则的化遍历string每次删除一个字符,然后判断是否是回文,只要有一个是的化,就返回true,否则返回false,响应的代码如下所示,但是这个的时间复杂度为O(n^2),会造成超时问题
public boolean validPalindrome(String s) {
//思路一:最初的思路就是要遍历整个字符串,然后挨个删除,看是否能组成回文,
//先判断原字符串是否为回文
if(valid(s)){
return true;
}else{
//删除一个字符后看下是否可以组成回文,只要有一个满足条件就可以。
for(int i =0;i<s.length();i++){
String ss = s.substring(0,i)+s.substring(i+1,s.length());
if(valid(ss)){
return true;
}
}
return false;
}
}
public boolean valid(String s){
int n = s.length();
int l = 0,r=n-1;
while(l<r){
if(s.charAt(l)==s.charAt(r)){
l++;
r--;
}else{
return false;
}
}
return true;
}
下面这段代码的时间复杂度为O(n),判断最长的是否是回文,如果不是的化,左右分别减1,看下是否是回文,至少要保证一个成立,否则就是非回文
public boolean validPalindrome(String s) {
int l = 0,r = s.length()-1;
while(l<r){
char lc = s.charAt(l);
char rc = s.charAt(r);
if(lc == rc){
l++;
r--;
}else{
return valid(s,l+1,r)||valid(s,l,r-1);
}
}
return true;
}
public boolean valid(String s,int l,int r){
while(l<r){
if(s.charAt(l)==s.charAt(r)){
l++;
r--;
}else{
return false;
}
}
return true;
}
4. 种花问题
假设有一个很长的花坛,一部分地块种植了花,另一部分却没有。可是,花不能种植在相邻的地块上,它们会争夺水源,两者都会死去。
给你一个整数数组 flowerbed 表示花坛,由若干 0 和 1 组成,其中 0 表示没种植花,1 表示种植了花。另有一个数 n ,能否在不打破种植规则的情况下种入 n 朵花?能则返回 true ,不能则返回 false。
思路:
题目要求是否能在不打破规则的情况下插入n朵花,与直接计算不同,采用“跳格子”的解法只需遍历不到一遍数组,处理以下两种不同的情况即可:
【1】当遍历到index遇到1时,说明这个位置有花,那必然从index+2的位置才有可能种花,因此当碰到1时直接跳过下一格。
【2】当遍历到index遇到0时,由于每次碰到1都是跳两格,因此前一格必定是0,此时只需要判断下一格是不是1即可得出index这一格能不能种花,如果能种则令n减一,然后这个位置就按照遇到1时处理,即跳两格;如果index的后一格是1,说明这个位置不能种花且之后两格也不可能种花(参照【1】),直接跳过3格。
当n减为0时,说明可以种入n朵花,则可以直接退出遍历返回true;如果遍历结束n没有减到0,说明最多种入的花的数量小于n,则返回false。
public boolean canPlaceFlowers(int[] flowerbed, int n) {
//看不懂题目,为啥示例2就是false呢???原来给到的flowerbed数组为1的情况,是表示当前这个地方已经种了花了,所以这个题目是,给到的一块已经种了部分花的地块,问是否还能再种n个花进去。
//从头到尾遍历一遍即可
int i =0;
while(i<flowerbed.length){
if(flowerbed[i]==1){
i=i+2;
}else if(i==flowerbed.length-1||flowerbed[i+1]==0){
n--;
i=i+2;
}else{//i+1种了花了,所以i处不能种花
i=i+3;
}
}
if(n<=0){
return true;
}else{
return false;
}
}
5.买卖股票的最佳时机II
给定一个数组 prices ,其中 prices[i] 是一支给定股票第 i 天的价格。
设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。
注意:你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。
思路:
自己题解的思路:
选择好买入点和卖出点规则:
买入点:后面的减去前面的大于0就可以买入
卖出点:只要和买入的交易差有增大的情况就不卖,一旦后一天的交易差小于今天的交易差就卖。
买入和卖出的前提条件是:设置flag,买入后不能再买,卖出前先判断是否前面买入了。
同时需要注意边界条件:最后一天不能买入呀balabala。。。
public int maxProfit(int[] prices) {
//感觉这题不是贪心算法,像动态规划呢~~~????动态规划???no
//感觉像找相邻的小的数买入,大的数卖出
//找到买入点规则:剩余数组的排序最小的数的点买入;但是不能再最后一天买否则就没得卖了
//卖出规则:???只要和买入的交易差有增大的情况就不卖,一旦后一天的交易差小于今天的交易差就卖。
//买入点选错了~~~[2,4,1],后减前为正就可以买入~~
int ans = 0;//最后总收入
boolean flag = false;
int buy = 0;
for(int i =0;i<prices.length;i++){
if(buyTime(prices,i)&&i!=prices.length-1&&flag==false){//买入时间计算
ans-=prices[i];
flag = true;
buy = prices[i];//记录买入的金额
}else{
//计算卖出时间
if(flag){//手里有货才能卖出
//最后一天必须卖掉
if(i==prices.length-1){
ans+=prices[i];
flag = false;
}else{//不是最后一天
int j = sealTime(prices,i,buy);
if(prices[j]-buy>0){
i=j;
ans+=prices[j];
flag = false;
}
}
}
}
}
return ans;
}
//计算买入时间
public boolean buyTime(int[] prices,int i){
int min = i,j=i+1;
if(j<prices.length && prices[j]-prices[i]>0){
return true;
}else{
return false;
}
}
//计算卖出时间,返回卖出时间下标i
public int sealTime(int[] prices,int i,int buy){//传入卖出时间(不是最后一天)和买入金额
int res = prices[i]-buy;
int j = i+1;
for(;j<prices.length;j++){
int shouru = prices[j]-buy;
if(shouru>res){
res = shouru;
}else{
return j-1;
}
}
if(j==prices.length){
return j-1;
}else{
return j;
}
}
官方思路:
由于股票的购买没有限制,因此整个问题等价于寻找x个不相交的区间(l,r] 使得他们的a[r]-a[l]和最大化.
同时我们注意到从(l,r]的和的最大化,等价于相邻数组的和的值所以,我们可以转化成求相邻和的最大值,如果有正向产出,则取正向值,否则我们取0即可。
**需要说明的是,贪心算法只能用于计算最大利润,计算的过程并不是实际的交易过程。**考虑题目中的例子 [1,2,3,4,5],实际上并不是交易了4次,而是1的时候买入,5的时候卖出
public int maxProfit(int[] prices) {
int ans = 0;
for(int i=0;i<prices.length-1;i++){
ans += Math.max(0,prices[i+1]-prices[i]);
}
return ans;
}
6.蓄水问题
给定 N 个无限容量且初始均空的水缸,每个水缸配有一个水桶用来打水,第 i 个水缸配备的水桶容量记作 bucket[i]。小扣有以下两种操作:
升级水桶:选择任意一个水桶,使其容量增加为 bucket[i]+1
蓄水:将全部水桶接满水,倒入各自对应的水缸
每个水缸对应最低蓄水量记作 vat[i],返回小扣至少需要多少次操作可以完成所有水缸蓄水要求。
注意:实际蓄水量 达到或超过 最低蓄水量,即完成蓄水要求。
提示:
1 <= bucket.length == vat.length <= 100
0 <= bucket[i], vat[i] <= 10^4
思路:需要注意提示中10^4,最多可以选择蓄水10000次,所以我们遍历每一次,从中选择最少的操作次数即可,需要前面注意的是,如果水缸vat是空的0,则不用蓄水,次数为0;
public int storeWater(int[] bucket, int[] vat) {
//最后提示很重要 0 <= bucket[i], vat[i] <= 10^4,说明最多的蓄水次数为10^4
//先判断vat水缸的最大容量,如果最大容量为0,则此时操作数为0
int maxVat = -1;
for(int va : vat){
if(va>maxVat){
maxVat = va;
}
}
if(maxVat == 0){
return 0;
}
int res = Integer.MAX_VALUE;//最终操作次数,
int n = vat.length;//vat.length == bucket.length所以此处任选其一即可。
//开始遍历蓄水次数
for(int i =1;i<=10000;i++){
int cur = i;//蓄水为i次时的总操作次数,为蓄水次数(cur = 蓄水次数i+升级次数)升级次数后面来计算
for(int j = 0;j<n;j++){//遍历n个桶和水缸
//要达到蓄水次数为i,则水桶的容量需要为vat[j]/i向上取整;
int per = (vat[j]+i-1)/i;//此处达到向上取整的目的
cur += Math.max(0,per-bucket[j]);//每个桶需要升级的次数
}
res = Math.min(res,cur);//取这1000次的最小值
}
return res;
}
7. 卡车上的最大单元数
请你将一些箱子装在 一辆卡车 上。给你一个二维数组 boxTypes ,其中 boxTypes[i] = [numberOfBoxesi, numberOfUnitsPerBoxi] :
numberOfBoxesi 是类型 i 的箱子的数量。
numberOfUnitsPerBoxi 是类型 i 每个箱子可以装载的单元数量。
整数 truckSize 表示卡车上可以装载 箱子 的 最大数量 。只要箱子数量不超过 truckSize ,你就可以选择任意箱子装到卡车上。
返回卡车可以装载 单元 的 最大 总数。
思路:典型的贪心算法,每次选择贡献率最大的值。
public int maximumUnits(int[][] boxTypes, int truckSize) {
//先需要根据numberOfUnitsPerBoxi也就是boxTypes[i][1]对boxTypes进行排序,
Arrays.sort(boxTypes, new Comparator<int[]>() {
@Override
public int compare(int[] o1, int[] o2) {
return o2[1]-o1[1];
}
});
int total = 0;
for(int i = 0;i<boxTypes.length;i++){
if(boxTypes[i][0]<=truckSize){
total += boxTypes[i][0]*boxTypes[i][1];
truckSize -= boxTypes[i][0];
}else{
total+= truckSize*boxTypes[i][1];
break;
}
}
return total;
}
8.非递增顺序的最小子序列
给你一个数组 nums,请你从中抽取一个子序列,满足该子序列的元素之和 严格 大于未包含在该子序列中的各元素之和。
如果存在多个解决方案,只需返回 长度最小 的子序列。如果仍然有多个解决方案,则返回 元素之和最大 的子序列。
与子数组不同的地方在于,「数组的子序列」不强调元素在原数组中的连续性,也就是说,它可以通过从数组中分离一些(也可能不分离)元素得到。
注意,题目数据保证满足所有约束条件的解决方案是 唯一 的。同时,返回的答案应当按 非递增顺序 排列。
思路:需要注意的是,本题目最终输出的子序列可以是从数组中随机找的,不用非得连续的,所以就排序从大到小找呗~~
public List<Integer> minSubsequence(int[] nums) {
ArrayList<Integer> list = new ArrayList<Integer>();
if(nums.length == 1){
list.add(nums[0]);
return list;
}
int sum = 0;
for(int i =0;i<nums.length;i++){
sum += nums[i];
}
int res = 0;//(sum+1)/2;//寻找结果大于等于res的最小的子序列
if(sum % 2==0){
res = sum / 2 +1;
}else{
res = (sum+1)/2;
}
//相邻序列求和
int ans = 0;
Arrays.sort(nums);
for(int i =nums.length-1;i>=0;i--){
ans += nums[i];
list.add(nums[i]);
if(ans>=res){
break;
}
}
return list;
}
9.移掉 K 位数字
给你一个以字符串表示的非负整数 num 和一个整数 k ,移除这个数中的 k 位数字,使得剩下的数字最小。请你以字符串形式返回这个最小的数字。
示例 1 :
输入:num = “1432219”, k = 3
输出:“1219”
解释:移除掉三个数字 4, 3, 和 2 形成一个新的最小的数字 1219 。
思路:核心思路就是从左到右进行遍历,直到右边小于左边的数,就把左侧的数删除掉,直到遍历完字符串为止。
需要考虑特殊情况,如果右面一直大于左侧的,则需要从后向前删除
同时如果出现了0200,这种前面的出现的有0则将前面的0删除掉
考虑这些情况,我们可以选择双端队列
public String removeKdigits(String num, int k) {
//删除k个字符
Deque<Character> deque = new LinkedList<Character>();
int n = num.length();
for(int i =0;i<n;i++){
char c= num.charAt(i);
while(!deque.isEmpty() && k>0 && c < deque.peekLast()){
deque.pollLast();
k--;
}
deque.offer(c);
}
for(int i=0;i<k;i++){
deque.pollLast();
}
StringBuilder str = new StringBuilder();
boolean flag = true;
while(!deque.isEmpty()){
char c = deque.pollFirst();
if(flag && c=='0'){
continue;
}
flag = false;
str.append(c);
}
return str.toString().equals("") ? "0" : str.toString();
}
10.不同字符的最小子序列
返回 s 字典序最小的子序列,该子序列包含 s 的所有不同字符,且只包含一次。
思路:此题和上一题类似,是上一题的升级版
需要有两个数据结构来存储特定的数据:
1、数组vis[],用于存放此字符是否存在于stack中;
2、map,用于存放此字符在string中出现了几次,每用一次就-1;
然后在从左到右遍历数组,判断是否存入到stack(stack其实就是一个双端队列)中
可以存入stack的条件是:该字符没有使用过;同时stak内的字符从底到上的顺序应该按照字符由小到大的顺序,除非该字符的map值已经为0了,就是后面不能再有该字符了。
public String smallestSubsequence(String s) {
//需要两个数据结构来存储,一个存储某字符出现的次数,每次用过之后在-1;一个用于记录stak中已经存有该字符
boolean[] vis = new boolean[26];
HashMap<Character,Integer> map = new HashMap<Character, Integer>();
for(int i =0;i<s.length();i++){
if(map.get(s.charAt(i))!=null){
map.put(s.charAt(i),map.get(s.charAt(i))+1);
}else{
map.put(s.charAt(i),1);
}
}
Deque<Character> stack = new LinkedList<Character>();//stack中的数据是需要从底向上,从小到大的;
for(int i =0;i<s.length();i++){
char c = s.charAt(i);
if(!vis[c-'a']){//s.charAt(i)没有被访问过
while(!stack.isEmpty() && stack.peekLast()>c){
if(map.get(stack.peekLast())!=0){//后面还有该字符
char ch = stack.pollLast();
vis[ch -'a']=false;
}else{//
break;
}
}
stack.offerLast(c);
vis[c-'a']=true;
}
map.put(c,map.get(c)-1);//每遍历过一次后,c的map值就要变化,无论是否已经放入到stack中,
}
String str = "";
while(!stack.isEmpty()){
char c = stack.pollFirst();
str +=c;
}
return str;
}
11.拼接最大数
给定长度分别为 m 和 n 的两个数组,其元素由 0-9 构成,表示两个自然数各位上的数字。现在从这两个数组中选出 k (k <= m + n) 个数字拼接成一个新的数,要求从同一个数组中取出的数字保持其在原数组中的相对顺序。
求满足该条件的最大数。结果返回一个表示该最大数的长度为 k 的数组。
说明: 请尽可能地优化你算法的时间和空间复杂度。
输入:
nums1 = [3, 4, 6, 5]
nums2 = [9, 1, 2, 5, 8, 3]
k = 5
输出:
[9, 8, 6, 5, 3]
https://leetcode-cn.com/problems/create-maximum-number/
运用分治思想,求两个数组分别组成的最大数,然后合并即可。