目录
贪心法
- 把整个问题分解成多个步骤,在每个步骤都选取当前步骤的最优方案,直到所有步骤结束;在每一步都不考虑对后续步骤的影响,在后续步骤中也不再回头改变前面的选择。
- 不足之处:
- 贪心算法并不能保证获得全局最优解,但总能获得局部最优解
贪心算法只能确定某些问题的可行性范围
在解决问题时,如果具备以下特点,可以考虑选用贪心算法:
最优子结构: 问题的最优解可以通过子问题的最优解推导而来。换句话说,在进行局部最优选择时能够保证全局最优。
贪心选择性质: 对于每个子问题,都做出当前看起来最优的选择,而不考虑未来的情况。这意味着在每一步都应该做出一个贪心选择,而不需要回溯或者重新评估之前的选择。
无后效性: 即某个状态的后继状态不受前面状态的影响。在贪心算法中,每个阶段的最优解依赖于之前各个阶段的状态,而不依赖于其他阶段的状态。
可证明性: 贪心选择的正确性可以通过数学归纳法或反证法等方式进行证明。
常见的适合使用贪心算法的问题包括最小生成树、最短路径、区间调度、背包问题(部分背包、分数背包)、活动选择等。在这些问题中,贪心算法通常能够快速给出高效的近似解。
与动态规划的区别
贪心算法与动态规划的不同在于它对每个子问题的解决方案都做出选择,不能回退。动态规划则会保存以前的运算结果,并根据以前的结果对当前进行选择,有回退功能。
一、 正整数分解使得乘积最大问题
问题描述:
设n是一个正整数。现在要求将n分解为若干个自然数之和,且使这些自然数的乘积最大。
分析:
将这个大问题分解为两个小问题:
(1)这些自然数是互不相同的
(2)这些自然数可以是相同的
1不会增大乘积,反而会占据和,所以分解出的数中不应有 1
先找几个数作例子,找规律
注意应用到实际问题时,可能存在两个问题:
1、乘积出来的数太大,题目要求返回取余后的结果即可
解决办法:一边乘积,一边取余,而非全部乘积完成后取余
2、分解出来的数太多,把它们乘积到一起会超时
解决办法:快速幂(不用递归)
见下面的第二题
1、不同的自然数
将其分解成连续的整数,从2开始,2,3,4,……将剩余的数按照从后往前的次序一次均匀分配。
package no1_1;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
int n=input.nextInt();
int k=2; // 从2开始分解成连续的数
// 创建一个包含100个整数的数组
int in[]=new int[100];
int i=0;
// 把n分成2,3,4,……一组连续的数字
while(n>=k) {
in[i++]=k;
n-=k;
k++;
}
// 如果有剩余的数,从后往前加到前面分好的一组连续数字中
if(n!=0) {
// 如果分剩的数与上一个减数相同,先对其加1,才能保证前面的减数都能均匀分配
if(n==in[i-1]) {
in[i-1]++;
n--;
}
// 从后往前均匀分配
for(int j=0;j<n;j++) {
in[i-1-j]++;
}
}
// 初始化乘积result为1
int result=1;
// 计算最大乘积
for(int j=0;j<=i-1;j++) {
result*=in[j];
}
System.out.println(result);
}
}
2、可以有相同的自然数
主要将 n 分解成2和3,主要是3。
package no1_1;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
int n=input.nextInt();
// 创建一个包含100个整数的数组
int in[]=new int[100];
int i=0;
// 当n不等于2和4时,按3进行分解
while(n!=2 && n!=4) {
in[i++]=3;
n-=3;
}
// 当n不等于0时,按2继续分解
while(n!=0) {
in[i++]=2;
n-=2;
}
// 初始化乘积result为1
int result=1;
// 计算最大乘积
for(int j=0;j<=i-1;j++) {
result*=in[j];
}
System.out.println(result);
}
}
二、数的潜能
分析
package no1_1;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
long n=input.nextLong();
if(n==1) {
System.out.println(1);
}else {
long threeNums=n/3;//n最多能分出多少个3
int result=1;
if(n%3==1) {//分解成threeNums个3和两个2,(4是特殊的例子,拆分成两个2时,乘积最大)
threeNums--;
result=4;
}else if(n%3==2) {//分解成threeNums个3和一个2
result=2;
}
result=binpow(result,3,threeNums,5218);
System.out.println(result);
}
}
//使用二进制快速幂算法计算 baseNumber 的 power 次幂对 modNumber 取模的结果
public static int binpow(int result,int baseNumber,long power,int modNumber) {
result%=modNumber;// 对底数取模,防止溢出
baseNumber%=modNumber;// 对底数取模,防止溢出
while(power>0) {
if((power&1)==1) {
result=result*baseNumber%modNumber;// 如果 幂 的当前位为 1,则更新结果
}
baseNumber=baseNumber*baseNumber%modNumber;// 底数自乘取模,相当于2次幂后取模
power>>=1;//power 右移一位
}
return result;
}
}
三、最大分解
分析
- n=a0>a1>a2>……>ap,a(i+1)是a(i)的最大约数,
- 比如10>5>1, 5是10不等于自身的最大约数,1是5不等于自身的最大约数
- 找到每层不等于自身的最大约数即可
package no1_1;
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner input=new Scanner(System.in);
int n=input.nextInt();
long sum=0;
while(n!=1) {//当n==1时,就不能再分解下去了
//当i==n时,n为质数,不等于它自身的最大约数即为1
for(int i=2;i<=n;i++) {
if(n%i==0) {
n=n/i;
sum+=n;
break;
}
}
}
System.out.println(sum);
}
}
四、平均
题目描述
有一个长度为 n 的数组(n 是 10 的倍数),每个数 ai 都是区间 [0, 9] 中的整数。小明发现数组里每种数出现的次数不太平均,而更改第 i 个数的代价为bi,他想更改若干个数的值使得这 10 种数出现的次数相等(都等于n/10),请问代价和最少为多少。
输入格式
输入的第一行包含一个正整数 n 。
接下来 n 行,第 i 行包含两个整数 ai , bi ,用一个空格分隔。
输出格式
输出一行包含一个正整数表示答案。
样例输入
10
1 1
1 2
1 3
2 4
2 5
2 6
3 7
3 8
3 9
4 10
样例输出
27
提示
只更改第 1, 2, 4, 5, 7, 8 个数,需要花费代价 1 + 2 + 4 + 5 + 7 + 8 = 27 。
对于 20% 的评测用例,n ≤ 1000;
对于所有评测用例,n ≤ 100000, 0 < bi ≤ 2 × 105 。
分析:
- 用一个count[]数组记录10种数字出现的次数
- 按修改代价从低到高排序原数组
- 从前往后遍历原数组,如果当前位置的数字次数大于平均次数,则该位置的数字必定要修改,至少改成哪个数字,不需要考虑,只需判断哪些位置需要修改即可
- 只需要找到所有出现数字次数大于平均次数的位置,把它们修改的代价和加起来即可
import java.util.*;
public class Main{
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int n=sc.nextInt();
int[][] t = new int[n][2];//t[i][0]代表ai,t[i][1]代表bi
int targetCount = n / 10; // 目标出现次数
long TotalCost=0;//总代价
int[] count = new int[10];//count[i]表示数字i已经出现的次数
for(int i=0;i<n;i++) {
t[i][0] = sc.nextInt();
t[i][1] = sc.nextInt();
}
Arrays.sort(t, (a, b) -> Integer.compare(b[1], a[1]));//对t数组按代价bi的大小降序排序
for(int i=0;i<n;i++) {
if(count[t[i][0]] < targetCount)//优先将代价大的元素标记为已经出现,若出现次数小于targetCount,count[ai]++;
count[t[i][0]]++;
else {//此时count[ai]超过了targetCount,必须将这个ai修改
TotalCost+=t[i][1];
}
}
System.out.println(TotalCost);
}
}
五、55. 跳跃游戏
给你一个非负整数数组
nums
,你最初位于数组的 第一个下标 。数组中的每个元素代表你在该位置可以跳跃的最大长度。判断你是否能够到达最后一个下标,如果可以,返回
true
;否则,返回false
。示例 1:
输入:nums = [2,3,1,1,4] 输出:true 解释:可以先跳 1 步,从下标 0 到达下标 1, 然后再从下标 1 跳 3 步到达最后一个下标。示例 2:
输入:nums = [3,2,1,0,4] 输出:false 解释:无论怎样,总会到达下标为 3 的位置。但该下标的最大跳跃长度是 0 , 所以永远不可能到达最后一个下标。提示:
1 <= nums.length <= 104
0 <= nums[i] <= 105
class Solution {
public boolean canJump(int[] nums) {
int maxReach = 0; // 当前能够到达的最远位置
int n = nums.length;
for (int i = 0; i < n; i++) {
if (i > maxReach) { // 当前位置已经超出最远可到达位置
return false;
}
maxReach = Math.max(maxReach, i + nums[i]); // 更新最远可到达位置
if (maxReach >= n - 1) { // 已经可以到达最后一个位置
return true;
}
}
return false;
}
}
六、122. 买卖股票的最佳时机 II
给你一个整数数组
prices
,其中prices[i]
表示某支股票第i
天的价格。在每一天,你可以决定是否购买和/或出售股票。你在任何时候 最多 只能持有 一股 股票。你也可以先购买,然后在 同一天 出售。
返回 你能获得的 最大 利润 。
示例 1:
输入:prices = [7,1,5,3,6,4] 输出:7 解释:在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6 - 3 = 3 。 总利润为 4 + 3 = 7 。示例 2:
输入:prices = [1,2,3,4,5] 输出:4 解释:在第 1 天(股票价格 = 1)的时候买入,在第 5 天 (股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5 - 1 = 4 。 总利润为 4 。示例 3:
输入:prices = [7,6,4,3,1] 输出:0 解释:在这种情况下, 交易无法获得正利润,所以不参与交易可以获得最大利润,最大利润为 0 。提示:
1 <= prices.length <= 3 * 104
0 <= prices[i] <= 104
class Solution {
public int maxProfit(int[] prices) {
int maxProfit = 0; // 初始化最大利润为0
for (int i = 1; i < prices.length; i++) {
if (prices[i] > prices[i - 1]) { // 如果后一天的价格高于前一天,则进行交易
maxProfit += prices[i] - prices[i - 1];
}
}
return maxProfit;
}
}
七、134. 加油站
在一条环路上有
n
个加油站,其中第i
个加油站有汽油gas[i]
升。你有一辆油箱容量无限的的汽车,从第
i
个加油站开往第i+1
个加油站需要消耗汽油cost[i]
升。你从其中的一个加油站出发,开始时油箱为空。给定两个整数数组
gas
和cost
,如果你可以按顺序绕环路行驶一周,则返回出发时加油站的编号,否则返回-1
。如果存在解,则 保证 它是 唯一 的。示例 1:
输入: gas = [1,2,3,4,5], cost = [3,4,5,1,2] 输出: 3 解释: 从 3 号加油站(索引为 3 处)出发,可获得 4 升汽油。此时油箱有 = 0 + 4 = 4 升汽油 开往 4 号加油站,此时油箱有 4 - 1 + 5 = 8 升汽油 开往 0 号加油站,此时油箱有 8 - 2 + 1 = 7 升汽油 开往 1 号加油站,此时油箱有 7 - 3 + 2 = 6 升汽油 开往 2 号加油站,此时油箱有 6 - 4 + 3 = 5 升汽油 开往 3 号加油站,你需要消耗 5 升汽油,正好足够你返回到 3 号加油站。 因此,3 可为起始索引。示例 2:
输入: gas = [2,3,4], cost = [3,4,3] 输出: -1 解释: 你不能从 0 号或 1 号加油站出发,因为没有足够的汽油可以让你行驶到下一个加油站。 我们从 2 号加油站出发,可以获得 4 升汽油。 此时油箱有 = 0 + 4 = 4 升汽油 开往 0 号加油站,此时油箱有 4 - 3 + 2 = 3 升汽油 开往 1 号加油站,此时油箱有 3 - 3 + 3 = 3 升汽油 你无法返回 2 号加油站,因为返程需要消耗 4 升汽油,但是你的油箱只有 3 升汽油。 因此,无论怎样,你都不可能绕环路行驶一周。提示:
gas.length == n
cost.length == n
1 <= n <= 105
0 <= gas[i], cost[i] <= 104
分析:
- 只遍历一遍数组,totalGas记录了走过的站点没加够的油量,如果后面没走过的站点能加的油不能弥补这个空洞,就是失败
class Solution {
public int canCompleteCircuit(int[] gas, int[] cost) {
int totalGas = 0; // 总剩余汽油量
int currentGas = 0; // 当前剩余汽油量
int startStation = 0; // 起始加油站
for (int i = 0; i < gas.length; i++) {
totalGas += gas[i] - cost[i]; // 计算总剩余汽油量
currentGas += gas[i] - cost[i]; // 计算当前剩余汽油量
// 如果当前剩余汽油量小于0,则无法到达下一个加油站,将起点设置为下一个加油站,并将当前剩余汽油量置零
if (currentGas < 0) {
startStation = i + 1;
currentGas = 0;
}
}
// 如果总剩余汽油量小于0,则无法绕行一圈,返回-1;否则返回起始加油站
return totalGas < 0 ? -1 : startStation;
}
}