# 贪心算法与回溯算法

# 1 贪心算法

# 1 原理

  1. 确定问题的最优子结构
  2. 设计一个递归算法(对活动选择问题, 我们给出了递归式)
  3. 证明如果我们做出了一个贪心选择, 只剩下一个字问题
  4. 证明贪心选择总是安全的
  5. 实际一个递归算法实现贪心策略
  6. 将递归算法转换为迭代算法
1. 将最优化问题转化为这样的形式, 对其做出一次选择, 只是剩下下一个子问题需要求解
2. 证明做出了贪心选择后,  原问题总存在最优解, 即贪心选择总是安全的
3. 证明做出贪心选择后, 剩余的子问题满足性质: 
其最优解与贪心选择组合即可得到原问题的最优解, 这样就得到了,最优子结构

# 2 快速想法

保证每次操作都是局部最优的,并且最后得到的结果是全局最优的。

# 2 例题

  • 知识的与不同的题型结合

# 1 分发饼干

假设你是一位很棒的家长,想要给你的孩子们一些小饼干。但是,每个孩子最多只能给一块饼干。对每个孩子 i ,
都有一个胃口值 gi ,这是能让孩子们满足胃口的饼干的最小尺寸;并且每块饼干 j ,都有一个尺寸 sj 。
如果 sj >= gi ,我们可以将这个饼干 j 分配给孩子 i ,这个孩子会得到满足。
你的目标是尽可能满足越多数量的孩子,并输出这个最大数值。

注意:

你可以假设胃口值为正。
一个小朋友最多只能拥有一块饼干。

输入: [1,2,3], [1,1]
输出: 1

解释: 
你有三个孩子和两块小饼干,3个孩子的胃口值分别是:1,2,3。
虽然你有两块小饼干,由于他们的尺寸都是1,你只能让胃口值是1的孩子满足。
所以你应该输出1。
  • 证明:假设在某次选择中,贪心策略选择给当前满足度最小的孩子分配第 m 个饼干,第 m 个饼干为可以满足该孩子的最小饼干。假设存在一种最优策略,给该孩子分配第 n 个饼干,并且 m < n。我们可以发现,经过这一轮分配,贪心策略分配后剩下的饼干一定有一个比最优策略来得大。因此在后续的分配中,贪心策略一定能满足更多的孩子。也就是说不存在比贪心策略更优的策略,即贪心策略就是最优策略。
class Solution {
    public int findContentChildren(int[] g, int[] s) {
        Arrays.sort(g);
        Arrays.sort(s);
        int gi = 0, si = 0;
        
        while(gi < g.length && si < s.length ){
            if(g[gi] <= s[si]){
                gi++;
            }
            si++;
        }
        
        return gi;
    }
}

# 2 买卖股票的最佳时机

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

如果你最多只允许完成一笔交易(即买入和卖出一支股票),设计一个算法来计算你所能获取的最大利润。

注意你不能在买入股票前卖出股票。

示例 1:

输入: [7,1,5,3,6,4]

输出: 5

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 5 天(股票价格 = 6)的时候卖出,最大利润 = 6-1 = 5 。

​ 注意利润不能是 7-1 = 6, 因为卖出价格需要大于买入价格。

  • 记录遍历过的数组的最小值, 每个元素减去最小值与ans比较
public int maxProfit(int[] prices) {
    int profit = 0;
    for (int i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) {
            profit += (prices[i] - prices[i - 1]);
        }
    }
    return profit;
}

# 3 买卖股票的最佳时机 II

给定一个数组,它的第 i 个元素是一支给定股票第 i 天的价格。

设计一个算法来计算你所能获取的最大利润。你可以尽可能地完成更多的交易(多次买卖一支股票)。

**注意:**你不能同时参与多笔交易(你必须在再次购买前出售掉之前的股票)。

示例 1:

输入: [7,1,5,3,6,4]

输出: 7

解释: 在第 2 天(股票价格 = 1)的时候买入,在第 3 天(股票价格 = 5)的时候卖出, 这笔交易所能获得利润 = 5-1 = 4 。

​ 随后,在第 4 天(股票价格 = 3)的时候买入,在第 5 天(股票价格 = 6)的时候卖出, 这笔交易所能获得利润 = 6-3 = 3 。

  • 遍历数组,找出连续的vally peak,相加差值。
class Solution {
    public int maxProfit(int[] prices) {
        int i = 0;
        int valley = 0;
        int peak = 0;
        int maxprofit = 0;
        while (i < prices.length - 1) {
            while (i < prices.length - 1 && prices[i] >= prices[i + 1])
                i++;
            valley = prices[i];
            while (i < prices.length - 1 && prices[i] <= prices[i + 1])
                i++;
            peak = prices[i];
            maxprofit += peak - valley;
        }
        return maxprofit;
    }
}
  • 遍历数组, 每次都是最优解, 找到每次后一个比前一个大的一对数据
class Solution {
    public int maxProfit(int[] prices) {
       int len = prices.length;
        
        int sum =0;
        for(int  i = 1; i<len;  i++){
            if(prices[i-1] < prices[i]){
                sum += prices[i] -prices[i-1] ;
            }
        }
        return sum;
    }
}

# 4 加油站

在一条环路上有 N 个加油站,其中第 i 个加油站有汽油 gas[i] 升。

你有一辆油箱容量无限的的汽车,从第 i 个加油站开往第 i+1 个加油站需要消耗汽油 cost[i] 升。你从其中的一个加油站出发,开始时油箱为空。

如果你可以绕环路行驶一周,则返回出发时加油站的编号,否则返回 -1。

说明:

  • 如果题目有解,该答案即为唯一答案。
  • 输入数组均为非空数组,且长度相同。
  • 输入数组中的元素均为非负数。

示例 1:

输入:

gas = [1,2,3,4,5]

cost = [3,4,5,1,2]

输出: 3

class Solution {
  public int canCompleteCircuit(int[] gas, int[] cost) {
    int n = gas.length;

    int total_tank = 0;
    int curr_tank = 0;
    int starting_station = 0;
    for (int i = 0; i < n; ++i) {
      total_tank += gas[i] - cost[i];
      curr_tank += gas[i] - cost[i];
      // If one couldn't get here,
      if (curr_tank < 0) {
        // Pick up the next station as the starting one.
        starting_station = i + 1;
        // Start with an empty tank.
        curr_tank = 0;
      }
    }
    return total_tank >= 0 ? starting_station : -1;
  }
}

# 2 回溯算法

# 1 定义

#

# 1 精确定义

  • 回溯算法实际上一个类似枚举的搜索尝试过程,主要是在搜索尝试过程中寻找问题的解,当发现已不满足求解条件时,就 “回溯” 返回,尝试别的路径。回溯法是一种选优搜索法,按选优条件向前搜索,以达到目标。但当探索到某一步时,发现原先选择并不优或达不到目标,就退回一步重新选择,这种走不通就退回再走的技术为回溯法,而满足回溯条件的某个状态的点称为 “回溯点”。许多复杂的,规模较大的问题都可以使用回溯法,有“通用解题方法”的美称。

  • 回溯算法的基本思想是:从一条路往前走,能进则进,不能进则退回来,换一条路再试。

  • -回溯法的算法本质是:n 叉树的遍历

# 2 另一种视角

回溯法是一种系统搜索问题解空间的方法。为了实现回溯,需要给问题定义一个解空间。
说到底它是一种搜索算法。只是这里的搜索是在一个叫做解空间的地方搜索。
而往往所谓的dfs,bfs都是在图或者树这种数据结构上的搜索。
根据定义来看,要实现回溯,需要两点   1搜索, 2解空间


先看什么是解空间。
就是形如数组的一个向量[a1,a2,....,an]。这个向量的每个元素都是问题的部分解,
只有当这个数组的每一个元素都填满(得到全部解)的时候,才表明这个问题得到了解答。

再看搜索。
最简单的就是for循环,上面的向量有n个维度,因此就是n个for循环。

形如:
for(求a1位置上的解)
   for(求a2位置上的解)
      for(求a3位置上的解)
       ......
       ......
       for(求an位置上的解)
但是如果n是100?n是100000?那么如何回溯?

# 3 模板

void backtrack(int i,int n,other parameters){
  if( i == n){
    //get one answer
    record answer;
    return;
  }
//下面的意思是求解空间第i个位置上的下一个解
for(next ans in position i of solution space){
    backtrack(i+1,n,other parameters);
    }
}

# 2 用回溯法解题的一般步骤:

  1. 针对所给问题,确定问题的解空间: 首先应明确定义问题的解空间,问题的解空间应至少包含问题的一个(最优)解。
  2. 确定结点的扩展搜索规则
  3. 以深度优先方式搜索解空间,并在搜索过程中用剪枝函数避免无效搜索。

# 3 回溯法的一般实现:

回溯法一般有两种代码实现方案,递归方法和非递归方法。

# 4 回溯法结果

  • 找到一个可能存在的正确的答案
  • 在尝试了所有可能的分步方法后宣告该问题没有答案

# 4 实例

  • 数组中的 子集, 组合, 排列,
  • 树中所有路径,所有节点的结果,
  • 排列, 组合问题。

# ·1 组合

给定两个整数 n 和 k,返回 1 ... n 中所有可能的 k 个数的组合。

示例:

输入: n = 4, k = 2
输出:
[
  [2,4],
  [3,4],
  [2,3],
  [1,2],
  [1,3],
  [1,4],
]
class Solution {
   public List<List<Integer>> combine(int n, int k) {
    List<List<Integer>> combinations = new ArrayList<>();
    List<Integer> combineList = new ArrayList<>();
    backtracking(combineList, combinations, 1, k, n);
    return combinations;
}

private void backtracking(List<Integer> combineList, List<List<Integer>> combinations, int start, int k, final int n) {
    if (k == 0) {
        combinations.add(new ArrayList<>(combineList));
        return;
    }
    for (int i = start; i <= n - k + 1; i++) {  // 剪枝
        combineList.add(i);
        backtracking(combineList, combinations, i + 1, k - 1, n);
        combineList.remove(combineList.size() - 1);
    }
}
}
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class Solution {

    private List<List<Integer>> res = new ArrayList<>();

    private void findCombinations(int n, int k, int begin, Stack<Integer> pre) {
        if (pre.size() == k) {
            // 够数了,就添加到结果集中
            res.add(new ArrayList<>(pre));
            return;
        }
        // 关键在于分析出 i 的上界
        for (int i = begin; i <= n; i++) {
            pre.add(i);
            findCombinations(n, k, i + 1, pre);
            pre.pop();
        }
    }

    public List<List<Integer>> combine(int n, int k) {
        // 特判
        if (n <= 0 || k <= 0 || n < k) {
            return res;
        }
        // 从 1 开始是题目的设定
        findCombinations(n, k, 1, new Stack<>());
        return res;
    }
}