算法很美 笔记 8.贪心策略与动态规划

8.贪心策略与动态规划

动态规划和贪心算法都是一种递推算法均用局部最优解来推导全局最优解
是对遍历解空间的一种优化
当问题具有最优子结构时,可用动规,而贪心是动规的特例

什么是贪心策略-顾眼前

  • 遵循某种规则,不断(贪心地)选取当前最优策略,最终找到最优解

  • 难点:当前最优未必是整体最优

题1:硬币问题

有1元,5元,10元,50元,100元,500元的硬币各c1,c5,c10,c50,c100,c500枚.现在要用这些硬币来支付A元,最少需要多少枚硬币?

限制条件

0≤ C1,C5,C10,C50,C100,C500≤1000000000

0≤A≤1000000000

输入描述:

依次输入C1,C5,C10,C50,C100,C500和A,以空格分隔

输出描述:

输出最少所需硬币数,如果该金额不能由所给硬币凑出,则返回NOWAY

示例1

输入

3 2 1 3 0 2 620

输出

6
import java.util.Scanner;

public class 最少硬币 {
    final static int[] coin=new int[]{1,5,10,50,100,500};
    static int[] num=new int[6];
    static int count=0;
    private static void greedy(int money, int cur) {//money剩余的钱cur为当前可以使用的面值,递归从最大面值开始
        if(cur==0){//到了最小面值硬币了
            if(money>num[0]){//剩下钱比1元数量多
                System.out.println("NOWAY");
                System.exit(0);
            }else{
                count+=money;//剩下的全部用1元
                System.out.println(count);
                System.exit(0);
            }
        }
        int n=money/coin[cur];
        int min=Math.min(n,num[cur]);//取实际有的数目和最大使用数中最小的
        count+=min;//count为需要的硬币总数
        greedy(money-min*coin[cur],cur-1);//再继续用次大面值
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        String[] ttt=sc.nextLine().split(" ");//输入一行用空格分割
        for (int i = 0; i <6; i++) {
            num[i]=Integer.parseInt(ttt[i]);
        }
        int money=Integer.parseInt(ttt[6]);
        greedy(money,5);
    }
}

题目链接

题2:快速划船

N个人希望只乘一条船过河,每条船最多只能载两个人。因此,必须安排谁去与回来,以便所有人最快过河。每个人都有不同的划船速度;两个人速度取决于较慢者。请给出时间最短的策略。

输入值

输入的第一行包含一个整数T(1 <= T <= 20),即测试用例的数量。然后是T例。每例的第一行N,第二行包含N个人过河的秒数。每个案例前面都有一个空白行。人数不超过1000,每人的时间不超过100秒

输出量

对于每个测试用例,打印一行N个人穿过河流所需的总秒数。

样本输入

1
4
1 2 5 10

样本输出

17

  • 一种可能最快方案:最快的1,2过去,回来1(其实两个均可,结果一样),这样最快的1,2在两边,左边选最慢两个的过去,回来2,这样先运输最慢的两个,而且使慢的不参与返回,始终让最快的1,2参与返回,总共需要2+1+2+最后两名比较大的

  • 第二种可能最快方案:用最快的1去带最慢的,让最快1回来,再让用最快的1去带第二慢的,让最快1回来
import java.util.Arrays;
import java.util.Scanner;

public class 快速划船 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int T=sc.nextInt();
        for (int i = 0; i < T; i++) {
            int N=sc.nextInt();
            int[] time=new int[N];
            for (int j = 0; j < N; j++) {
                time[j]=sc.nextInt();//输入一行数字会以空格为分割
            }
            Arrays.sort(time);//题目中没有说给出的秒数是有序的
            int res=0,left=N;//左侧是渡河的起点,left代表左侧的剩余人数
            while(left>0){
                if(left==1){
                    res+=time[0];
                    break;//不要忘记
                }
                if(left==2){
                    res+=time[1];
                    break;//不要忘记只剩下2人时,人数要-2,或者break,使循环结束
                }
                if(left==3){
                    res+=time[0]+time[1]+time[2];
                    break;//不要忘记
                }
                if(left>3){
                    //1,2出发,1返回,最后两名出发,2返回
                    int x1=time[0]+2*time[1]+time[left-1];
                    //1和倒数第1出发,1返回,1和倒数第2出发,1返回
                    int x2=2*time[0]+time[left-1]+time[left-2];
                    int m=Math.min(x1,x2);
                    res+=m;
                    left-=2;//人数要-2
                }
            }
            System.out.println(res);
        }
    }
}

题目链接

题3:区间调度问题

有n项工作,每项工作分别在s~i~时间开始,在t~i~时间结束.对于每项工作,你都可以选择参与与否.如果选择了参与,那么自始至终都必须全程参与.此外,参与工作的时间段不能重复(即使是开始的瞬间和结束的瞬间的重叠也是不允许的).你的目标是参与尽可能多的工作,那么最多能参与多少项工作呢?

1≤n≤100000

1≤s~i~≤t~i~≤10^9^

输入:

第一行:n
第二行:n个整数空格隔开,代表n个工作的开始时间
第三行:n个整数空格隔开,代表n个工作的结束时间

样例输入:

5
1 3 1 6 8
3 5 2 9 10

样例输出:

3

说明:选取工作1,3,5

使用贪心算法,将所有的工作按结束时间的先后,从小到大排列,然后以第一个结束时间为初始值,保证了从起始点到该点之间,该事件的时间最短,判断下一个事件开始时间是否大于结束时间,若大于则执行该事件,更新初始值,继续判断下一事件,否则跳过该事件,判断下一个事件。

public class Case03_区间调度问题 {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    int[] s = new int[n];
    int[] t = new int[n];
    Job[] jobs = new Job[n];
    for (int i = 0; i < n; i++) {
      s[i] = sc.nextInt();
    }
    for (int i = 0; i < n; i++) {
      t[i] = sc.nextInt();
    }
    for (int i = 0; i < n; i++) {
      jobs[i] = new Job(s[i], t[i]);
    }

    Arrays.sort(jobs);
    int res = f(n, jobs);
    System.out.println(res);
  }

  private static int f(int n, Job[] jobs) {
    int cnt = 1;
    int y = jobs[0].t;//选择结束时间最早的
    for (int i = 0; i < n; i++) {
      if (jobs[i].s > y) {//从结束时间早到晚选择合适的
        cnt++;
        y = jobs[i].t;//更新结束时间
      }
    }
    return cnt;
  }

  /**
   * 必须实现排序规则
   */
  private static class Job implements Comparable<Job> {
    int s;//开始时间
    int t;//结束时间

    public Job(int s, int t) {
      this.s = s;
      this.t = t;
    }

    @Override
    public int compareTo(Job other) {
      int x = this.t - other.t;//按照结束时间从小到大排序
        return x;
    }
  }
}

题4:区间选点问题

给定n个整数闭区间,以及他们需要命中点的数目,设计程序求出最少需要多少个点

输入值

第一行间隔数n(1 <= n <= 50000)。以下n行每行三个整数之间用空格隔,ai、bi、ci分别为区间两端点,需要命中的点数。因此0 <= ai <= bi <= 50000和1 <= ci <= bi-ai + 1。

输出量

最少需要的点数

样本输入

5
3 7 3
8 10 3
6 8 1
1 3 1
10 11 1

样本输出

6
import java.util.Arrays;
import java.util.Scanner;

public class 区间选点问题I {
    static class Interval implements Comparable<Interval>{
        int s;//区间开始时间
        int e;//结束时间
        int n;//命中个数
        public Interval(int s,int e,int n){
            this.s=s;
            this.e=e;
            this.n=n;
        }
        public int compareTo(Interval o) {
            return this.e-o.e;//按照结束时间从小到大排序
        }
    }
    private static int sum(int[] record, int s, int e) {//统计record的s-e多少标记
        int count=0;
        for (int i = s; i <=e; i++) {
            if(record[i]==1){
                count++;
            }
        }
        return count;
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int n=sc.nextInt();
        Interval[] intervals=new Interval[n];
        for (int i = 0; i < n; i++) {
            intervals[i]=new Interval(sc.nextInt(),sc.nextInt(),sc.nextInt());
        }
        Arrays.sort(intervals);
        int[] record=new int[intervals[n-1].e+1];//统计哪些点被标记    
        // 第一个元素不用,最大个数是最后一个区间的结束为止
        for (int i = 0; i < n; i++) {
            int s=intervals[i].s;
            int e=intervals[i].e;
            intervals[i].n-=sum(record,s,e);//n要减去已经标记的点
            while(intervals[i].n>0){
                if(record[e]==0){//当前区间最后一个元素没有被标记过
                    record[e]=1;//标记
                    intervals[i].n--;//需要包含的点数--
                    e--;//end往前移动
                }else{//标记过end前移动
                    e--;
                }
            }
        }
        int count=0;
        for (int i = 0; i < record.length; i++) {
            if(record[i]==1){
                count++;
            }
        }
        System.out.println(count);
    }
}

import java.util.Arrays;
import java.util.Scanner;
public class Case04_区间选点问题II {
  public static void main(String[] args) {
    Scanner sc = new Scanner(System.in);
    int n = sc.nextInt();
    Interval[] intervals = new Interval[n];
    for (int i = 0; i < n; i++) {
      intervals[i] = new Interval(sc.nextInt(), sc.nextInt(), sc.nextInt());
    }
    Arrays.sort(intervals);//按区间右端点排序

    int max = intervals[n - 1].t;//右端最大值
    int[] axis = new int[max + 1];
    int[] c = new int[max + 2];
    // int[] sums = new int[max + 1];
    for (int i = 0; i < n; i++) {
      //1.查阅区间中有多少个点
      int s = intervals[i].s;//起点
      int t = intervals[i].t;//终点
      int cnt = sum(t + 1, c, max + 1) - sum(s, c, max + 1);//sum(axis,s,t);//sums[t] - sums[s - 1];//效率低
      //  2.如果不够,从区间右端开始标记,遇标记过的就跳过
      intervals[i].c -= cnt;
      while (intervals[i].c > 0) {
        if (axis[t] == 0) {
          axis[t] = 1;
          update(t + 1, 1, c, max + 1);
          intervals[i].c--;
          t--;
        } else {
          t--;
        }
      }

    }
    System.out.println(sum(max + 2, c, max + 1));
  }

  /**
   * 更新树状数组c,注意i是项数,不是下标,而是下标+1*/
  private static void update(int i, int delta, int[] c, int n) {
    for (; i <= n; i += lowbit(i)) {
      c[i] += delta;
    }
  }

  /**
   * 前i项和,注意:i不是下标
   * @param i
   * @return
   */
  private static int sum(int i, int[] c, int n) {
    int sum = 0;
    if (i > n)
      i = n;
    for (; i > 0; i -= lowbit(i)) {
      sum += c[i];
    }
    return sum;
  }
  /**
   * 它通过公式来得出k,其中k就是该值从末尾开始1的位置。
   * 然后将其得出的结果加上x自身就可以得出当前节点的父亲节点的位置
   * 或者是x减去其结果就可以得出上一个父亲节点的位置。
   * 比如当前是6,二进制就是0110,k为2,那么6+2=8,C(8)则是C(6)的父亲节点的位置;
   * 相反,6-2=4,则是C(6)的上一个父亲节点的位置。*/
  static int lowbit(int x) {
    return x - (x & (x - 1));
  }

  private static class Interval implements Comparable<Interval> {
    int s;
    int t;
    int c;

    public Interval(int s, int t, int c) {
      this.s = s;
      this.t = t;
      this.c = c;
    }

    @Override
    public int compareTo(Interval other) {
      int x = this.t - other.t;
      if (x == 0)
        return this.s - other.s;
      else
        return x;
    }
  }

}

题目链接

题5:区间覆盖问题

农夫约翰正在分配他的N头(1 <= N <= 25,000)牛在谷仓周围做一些清洁工作。他一直想让一头母牛进行清理工作,并将一天分为T个时间段(1 <= T <= 1,000,000),第一个是间隔1,最后一个间隔是T。

每头牛只能在特定的时间段进行清洁工作。任何被选定的牛将在整个时间段内工作。

您的工作是帮助农夫约翰分配一些奶牛到时间段,以便(i)每个班次至少分配一头奶牛,并且(ii)尽可能少地母牛参与清洁工作。如果无法实现所有时间段内都有牛工作,则打印-1。

输入值

第1行:两个以空格分隔的整数:N和T

第2..N + 1行:每行包含母牛可以工作的时间段开始和结束时间。母牛在开始时间开始工作,在结束时间之后结束。

输出量

第1行:农夫约翰需要雇用的最小母牛数或如果无法实现所有时间段内都有牛工作,-1。

样本输入

3 10
1 7
3 6
6 10

样本输出

2

能覆盖start,最靠右的端点(t最大),当不能覆盖时,更新start=end+1,计数+1,判断如果头过了新的start,更新end,否则无解

子问题最优=》全局问题最优

import java.util.Arrays;
import java.util.Scanner;

public class 区间覆盖 {
    public static class Cow implements Comparable<Cow>{
        int s;
        int t;
        public Cow(int s,int t){
            this.s=s;
            this.t=t;
        }

        @Override
        public int compareTo(Cow cow) {
            return this.s-cow.s;//本-其他
        }
    }

    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int N=sc.nextInt();//牛的数量
        int T=sc.nextInt();//时间
        Cow[] cows=new Cow[N];
        for (int i = 0; i < N; i++) {
            cows[i]=new Cow(sc.nextInt(),sc.nextInt());
        }
        Arrays.sort(cows);
        int count=1;//初始为1,不是0。当换start时+1
        int start=1,end=1;
        for (int i = 0; i < N; i++) {
            int s=cows[i].s;
            int t=cows[i].t;
            if (i == 0 && s > 1) break;//最小开始时间都大于1
            if(s<=start){//该段开头start左面,跟新end
                end=Math.max(end,t);
            }else{//对应图片情况
                count++;
                start=end+1;//由于是区间,不是每个点的端点
                //例如比如[1,3] [4,7] ,中间时间段是连续的
                if(s<=start){//此段区间左边是否在start左边
                    end=Math.max(end,t);
                }else {//不在的话就不存在-1
                    break;
                }
            }
            if(end>=T){//不要忘记end超过了要求了,要退出
                break;
            }
        }
        if(end<T){
            System.out.println("-1");
        }else {
            System.out.println(count);
        }
    }
}

题目链接

题6:两头选出字典序最小

字典序最小问题
给一个定长为N的字符串S,构造一个字符串T,长度也为N。
起初,T是一个空串,随后反复进行下列任意操作

  1. 从S的头部删除一个字符,加到T的尾部
  2. 从S的尾部删除一个字符,加到T的尾部
    目标是最后生成的字符串T的字典序尽可能小
    1≤N≤2000
    字符串S只包含大写英文字母
    输入:字符串S
    输出:字符串T
    样本输入
6 
A 
C 
D 
B 
C 
B

样本输出

ABCBCD
import java.util.Scanner;

public class 字典序最小 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int N=sc.nextInt();
        char[] name=new char[N];
        for (int i = 0; i <N ; i++) {
            name[i]=sc.next().charAt(0);
        }
        int start=0;
        int end=name.length-1;
        int sum=0;
        while(start<=end){//如果start等于end,for循环只执行一次,flag没有被赋值,打印start或end都可以
            boolean flag = false;//是否选左边
            for(int i = 0; start+i<end-i; i++) {
                //中间临界情况
                //AA _ AA 1.AA A AA  2.AA B AA 所以上面可以不用判断start+i==end-i
                //AA_ _AA 1.AA AA AA  2.AA BB AA 3.AA AC AA
                if(name[start+i]<name[end-i]){
                    flag=true;
                    break;
                }else if(name[start+i]>name[end-i]){
                    flag=false;
                    break;
                }
            }
            if(flag){
                System.out.print(name[start++]);
            }else {
                System.out.print(name[end--]);
            }
            sum++;
            if(sum%80==0){
                System.out.println();
            }
        }
    }
}

题目链接

题7:最优装载

给出n个物体,第i个物体重量为wi。选择尽量多的物体,使得总重量不超过C。

import java.util.Arrays;
import java.util.Scanner;
public class Case07_最优装载问题 {
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt();
        int[] w = new int[n];
        for (int i = 0; i < n; i++) {
            w[i] = sc.nextInt();
        }
        int C = sc.nextInt();

        Arrays.sort(w);
        int ans = f(n, w, C);
        System.out.println(ans);
    }

    private static int f(int n, int[] w, int c) {
        int sum = 0;
        int cnt = 0;
        for (int i = 0; i < n; i++) {
            sum += w[i];
            if (sum <= c) {
                cnt++;
            } else {
                break;
            }
        }
        return cnt;
    }
}

题8:背包部分问题

有n个物体,第i个物体的重量为wi,价值为vi。在总重量不超过C的情况下让总价值尽量高。每一个物体都可以只取走一部分,价值和重量按比例计算。
求最大总价值
注意:每个物体可以只拿一部分,因此一定可以让总重量恰好为C。

尽可能能拿单价最高的,比如黄金,相同重量情况下价值最高,从大到小选 价值/重量高的,前面的物品全拿,最后一件根据重量拿部分

import java.util.Arrays;
public class Case08_部分背包问题 {
  public static void main(String[] args) {
    int[] w = {1, 2, 3, 4, 5};
    int[] v = {3, 4, 3, 1, 4};
    int n = w.length;
    double C = 10;
    Obj[] objs = new Obj[n];
    for (int i = 0; i < n; i++) {
      objs[i] = new Obj(w[i], v[i]);
    }

    Arrays.sort(objs);
    double c = C;
    double maxValue = 0;
    for (int i = n - 1; i >= 0; i--) {
      if (objs[i].w <= c) {
        maxValue += objs[i].v;
        c -= objs[i].w;
      } else {
        maxValue += objs[i].v * (c / objs[i].w);
        break;
      }
    }
    System.out.println(maxValue);
  }

  private static class Obj implements Comparable<Obj> {
    int w;
    int v;

    public Obj(int w, int v) {
      this.w = w;
      this.v = v;
    }

    public double getPrice() {
      return v / (double) w;
    }

    @Override
    public int compareTo(Obj o) {
      if (this.getPrice() == o.getPrice()) return 0;
      else if (this.getPrice() < o.getPrice()) return -1;
      else return 1;
    }

    @Override
    public String toString() {
      return "Obj{" +
          "w=" + w +
          ", v=" + v +
          ", price=" + getPrice() +
          '}';
    }
  }
}

小结

  • 最优子结构:对比dfs ,不是进行各种可选支路的试探,而是当下就可用某种策略确定选择,无需考虑未来(未来情况的演变也影响不了当下的选择)。

  • 只要一直这么选下去,就能得出最终的解,每一步都是当下(子问题)的最优解,结果是原问题的最优解,这叫做最优子结构。

  • 更书面的说法:如果问题的一个最优解中包含了子问题的最优解则该问题具有最优子结构。

  • 具备这类结构的问题,可以用局部最优解来推导全局最优解,可以认为是一种剪枝法,是对"dfs遍历法”的优化

  • 贪心:由_上一步的最优解推导下一步的最优解,而上一步之前的(历史)最优解则不作保留,区别动态规划,贪心是动态规划的特列

什么是动态规划

  • 动态规划方法代表了这一类问题(最优子结构or子问题最优性)的一般解法,是设计方法或者策略,不是具体算法

  • 本质是递推,核心是找到状态转移的方式,写出dp方程

  • 善于解决重叠子问题,同级子问题有交叉部分

  • 形式:

    • 记忆型递归

    • 递推

题9:01背包

Sidney想去Gandtom家玩。但Sidney家和Gandtom家之间是高低不平、坑坑洼洼的土路。所以他需要用他的背包装几袋稀的泥,在路上铺平一些干的土,使路变成平整的泥土,才能到Gandtom家见到Gandtom。
已知现在有n种稀的泥第i种稀的泥的质量为w~i~,体积为v~i~。Sidney的包能装体积不超过V的稀的泥。Sidney出门时携带的稀的泥的质量应该尽可能的大。在此前提下,携带的稀的泥的体积也应该尽可能的大。
  试求Sidney最多能携带多少质量的稀的泥与此时的最大体积上路。
Input
第一行有一个整数T,表示组数。
每组数据第一行有两个正整数n、V(0<n,V<=10^3^) 。
每组数据第二行有个n正整数,第i个数为w~i~(0<w~i~<=10^6^)。
每组数据第三行有个n正整数,第i个数为v~i~(0<v~i~<=10^3^)
Output
每组样例第一行输出两个整数。表示Sidney最多能携带多少质量的稀的泥与此时的最大体积上路。
Sample Input
2
5 3
1 2 3 4 5
1 1 1 1 1
3 7
1 2 1
3 5 3
Sample Output
12 3
2 6

import java.util.Arrays;
import java.util.Scanner;

public class _01背包 {
    static int n;
    static int V;
    static int[] weight;
    static int[] volume;
    static int[][] record;
    private static int dp(int i, int v) {//从i到最后可以选择 ,v为剩余可选体积
        if(i==n){//没有可以选择的了
            return 0;
        }
        if(v==0){//容量没有了
            return 0;
        }
        int w1=dp(i+1,v);//不选i泥土
        if(v>=volume[i]){//剩余容量可以选择第i个
            int w2=weight[i]+dp(i+1,v-volume[i]);////选则i泥土
            return Math.max(w1,w2);//比较出 选i泥土与不选i泥土重量最大的返回
        }else{
            return w1;//容量不够只能不选
        }
    }
    //记忆型递归
    private static int dp2(int i, int v) {//从i到最后可以选择 ,v为剩余可选体积
        if(i==n){//没有可以选择的了
            return 0;
        }
        if(v==0){//容量没有了
            return 0;
        }
        //1.计算之前先查询
        if(record[i][v]>=0){
            return record[i][v];
        }
        int w1=dp(i+1,v);//不选i泥土
        int result=0;
        if(v>=volume[i]){//剩余容量可以选择第i个
            int w2=weight[i]+dp(i+1,v-volume[i]);////选则i泥土
            result= Math.max(w1,w2);//比较出 选i泥土与不选i泥土重量最大的返回
        }else{
            result=w1;//容量不够只能不选
        }
        //2.计算之后做保存
        record[i][v]=result;
        return result;
    }
    private static int dp3(){
        //填写第一行 0号物品
        for(int i=1;i<V+1;i++){//当只有0号泥土可以选择 i表示可用容量1-V
            if(i>=volume[0]){//可用容量大于0号泥土容量
                record[0][i]=weight[0];
            }
        }
        //填写第一列  0-n-1号物品 体积为为0时 能承载的重量 全为0 已经初始化
        for (int i =1; i <n ; i++) {//一行一行初始化
            for (int j = 1; j <V+1 ; j++) {
                int i1;
                int i2=record[i-1][j];//不要i号泥土
                if(j>=volume[i]){//当前剩余容量大于第i号泥土,可以要
                    i1=weight[i]+record[i-1][j-volume[i]];//选第i泥土
                    record[i][j]=Math.max(i1,i2);
                }else {
                    record[i][j]= i2;
                }
            }
        }
        return record[n-1][V];
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int T=sc.nextInt();
        for (int i = 0; i < T; i++) {
            n=sc.nextInt();//泥的种类数
            V=sc.nextInt();
            weight=new int[n];
            volume=new int[n];
            for (int j = 0; j < n; j++) {
                weight[j]=sc.nextInt();
            }
            for (int j = 0; j < n; j++) {
                volume[j]=sc.nextInt();
            }
            int tmp=V;
            record=new int[n][V+1];//默认为0
//            for (int j = 0; j < n; j++) {
//                Arrays.fill(record[j],-1);
//            };
//            System.out.println(dp(0,V));//record需要全-1
//            System.out.println(dp2(0,V));//record需要全-1
            System.out.println(dp3());

        }
    }
}

import java.util.Scanner;

public class _01背包问题 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int T=sc.nextInt();
        for (int k = 0; k < T;k++) {
            int n=sc.nextInt();
            int V=sc.nextInt();
            int[] weight=new int[n];
            int[] volume=new int[n];
            for (int i = 0; i < n; i++) {
                weight[i]=sc.nextInt();
            }
            for (int i = 0; i < n; i++) {
                volume[i]=sc.nextInt();
            }
            //用一维数组
            int[] dp=new int[V+1];
            for (int i = 0; i < n; i++) {//可以选择的从这到前的物品
                for (int j =V; j>=volume[i] ; j--) {//体积要从大到小,这样,dp[j-volume[i]]的值才是上一次i-1的值,要不然是i的值
                    if(j==volume[i]||dp[j-volume[i]]>0)//新增的元素可能出开头的下一个j==volume[i],或者出现在现有元素的下一个
                        dp[j]=Math.max(dp[j],dp[j-volume[i]]+weight[i]);
                }
            }
            int max1=0,maxpos=0;
            for(int i=V;i>0;i--)//V从大到小选取
                if(dp[i]>max1){
                    max1=dp[i];
                    maxpos=i;
                }
            System.out.println(max1+" "+maxpos);
        }
    }
}

题目链接

纯01背包

骨头收集者有一个大袋子,里面装有V,而且在收集骨头的过程中,很明显,不同的骨骼具有不同的值和不同的体积,现在给定沿途的每个骨骼的值,您能否计算出骨骼收集器可以获得的总值的最大值?

输入值

第一行包含整数T,即案例数。
其后是T个案例,每个案例三行,第一行包含两个整数N,V(N <= 1000,V <= 1000),它们表示骨头种数和包的体积。第二行包含N个整数代表每个骨骼价值。第三行包含N个整数代表骨骼的体积。

输出量

每行一个整数,代表总价值最大值(该数字将小于2^31^)。

样本输入

1
5 10
1 2 3 4 5
5 4 3 2 1

样本输出

14
import java.util.Scanner;

public class _01背包纯 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int T=sc.nextInt();
        while(T>0){
            int N=sc.nextInt();//N为骨骼的种类数
            int V=sc.nextInt();//包的体积
            int[] value=new int[N];
            int[] volume=new int[N];
            for (int i = 0; i < N; i++) {
                value[i]=sc.nextInt();
            }
            for (int i = 0; i < N; i++) {
                volume[i]=sc.nextInt();
            }
            //二维数组
//            int[][] dp=new int[N+1][V+1];
//            for (int i = 1; i <=N; i++) {
//                for (int j = 0; j <=V ; j++) {//体积要从0开始
//                    if(j>=volume[i-1]){//当前体积j大于i号骨骼体积,可以加入
//                        dp[i][j]=Math.max(dp[i-1][j],value[i-1]+dp[i-1][j-volume[i-1]]);//比较加入与不加入哪个大
//                    }else {
//                        dp[i][j]=dp[i-1][j];//不加入
//                    }
//                }
//            }
//            System.out.println(dp[N][V]);
            //一位数组的方法
            int[] dp=new int[V+1];
            for (int i = 0; i <N; i++) {
                for (int j = V; j >=volume[i]; j--) {//要倒着减
                    dp[j]=Math.max(dp[j],value[i]+dp[j-volume[i]]);
                }
            }
            System.out.println(dp[V]);
            T--;
        }
    }
}

题目链接

一维数组思路

先考虑上面讲的基本思路如何实现,肯定是有一个主循环i=1..N,每次算出来二维数组f[i][0..V]的所有值。那么,如果只用一个数组f[0..V],能不能保证第i次循环结束后f[v]中表示的就是我们定义的状态f[i][v]呢?f[i][v]是由f[i-1][v]和f[i-1][v-c[i]]两个子问题递推而来,能否保证在推f[i][v]时(也即在第i次主循环中推f[v]时)能够得到f[i-1][v]和f[i-1][v-c[i]]的值呢?事实上,这要求在每次主循环中我们以v=V..0的顺序推f[v],这样才能保证推f[v]时f[v-c[i]]保存的是状态f[i-1][v-c[i]]的值。如果将v的循环顺序从上面的逆序改成顺序的话,那么则成了f[i][v]由f[i][v-c[i]]推知,与本题意不符,但它却是另一个重要的背包问题P02最简捷的解决方案,故学习只用一维数组解01背包问题是十分必要的。

参考博客

更详细背包总结

题10:钢条切割

Serling公司购买长钢条,将其切割为短钢条出售。切割工序本身没有成本支出。公司管理层希望知道最佳的切割方案。
假定我们知道Serling公司出售一段长为i英寸的钢条的价格为pi(i=1,2,…,单位为美元)。钢条的长度均为整英寸。

钢条切割问题是这样的:给定一段长度为n英寸的钢条和一个价格表pi(i=1,2,…n),求切割钢条方案,使得销售收益rn最大。

注意,如果长度为n英寸的钢条的价格pn足够大,最优解可能就是完全不需要切割。

下面另n=10,给出个长度的价格,求价值最大值

长度i 1 2 3 4 5 6 7 8 9 10
价格Pi 1 5 8 16 10 17 17 20 24 30
import java.util.Arrays;

public class 钢条切割 {
    static int n =10;
    static int[] p = {1, 5, 8, 16, 10, 17, 17, 20, 24, 30};
    static int[] vs = new int[n + 1];
    public static int r(int x){//记忆性递归
        if(x==0){
            return 0;
        }
        int max=0;//vs存储的是长度对应最大价值
        for (int i = 1; i <=x ; i++) {//分割成两段。i代表第一段长度可能性1-10英尺
            if(vs[x-i]==-1){//如果vs[x-i]没有就需要递归后结果存贮,有的话可以直接用
                vs[x-i]=r(x-i);
            }
            int v=p[i-1]+vs[x-i];//p[i-1]为i英尺的价值  +剩下长度最大价值
            max=Math.max(v,max);//如果更大就更新
        }
        return max;
    }
    public static int dp(int x){
        vs[0]=0;
        for (int i = 1; i <=n ; i++) {//拥有的钢条长度
            for (int j = 1; j <= i; j++) {//将i分割成两段,第一段长度j从1-i
                int temp=p[j-1]+vs[i-j];//p[j-1]为长度为j的价值+剩下长度的最优价值
                //由于i-j最大时是i-1,vs[i]之前已经求出最佳方案了
                vs[i]=Math.max(temp,vs[i]);//如果更大就更新
            }
        }
        return vs[x];
    }
    public static void main(String[] args) {
//        Arrays.fill(vs,-1);
//        System.out.println(r(n));
        System.out.println(dp(n));
    }
}

递归和记忆型递归都是自顶向下,dp是自底向上的,分析依赖

为了节约空间,可以使用滚动数组

题11:数字三角形

在数字三角形中寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。

​ 7

3 8

8 1 0

2 7 4 4

4 5 2 6 5

输入值

第一行包含一个整数N:三角形中的行数。接下来的N行描述了三角形的数据。三角形的行数大于1小于等于100,数字为 0 - 99

输出量

输出最大和

样本输入

5
7
3 8
8 1 0 
2 7 4 4
4 5 2 6 5

样本输出

30
import java.util.Scanner;

public class 数字三角形 {
    public static int maxSum(int[][] a, int i, int j) {
        if (i == a.length - 1) {
            return a[i][j];
        } else {
            //顶点的值+max(左侧支线的最大值,右侧支路的最大值)
            return a[i][j]+
                    max(maxSum(a, i + 1, j), maxSum(a, i + 1, j + 1));
        }
    }
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int N=sc.nextInt();
        int[][] a=new int[N][N];
        for (int i = 0; i < N; i++) {
            for (int j = 0; j <=i; j++) {
                a[i][j]=sc.nextInt();
            }
        }
//        int[][] rec=new int[N][N];
//        for (int i = 0; i < N; i++) {//初始化最后一行,还是原来的
//            rec[N-1][i]=a[N-1][i];
//        }
//        for (int i=N-2;i>=0;i--){//从倒数第二行往上初始化
//            for (int j = 0; j <=i; j++) {//每行从头初始化
//                rec[i][j]=a[i][j]+Math.max(rec[i+1][j],rec[i+1][j+1]);
//                //rec[i][j]最大值为原来的值加上rec[i][j]左下与右下最大的
//            }
//        }
//        System.out.println(rec[0][0]);
        int[] rec=new int[N];
        for (int i = 0; i < N; i++) {//初始化最后一行,还是原来的
           rec[i]=a[N-1][i];
        }
        for (int i=N-2;i>=0;i--){//从倒数第二行往上初始化
            for (int j = 0; j <=i; j++) {//每行从头初始化
                rec[j]=a[i][j]+Math.max(rec[j],rec[j+1]);
                //现rec[j]最大值为原来的值加上原rec[j]和rec[j+1]最大者
                //由于原来rec[j]的值被覆盖,但是rec[j+1] 要用的是rec[j+1]和rec[j+2]
                //所以可以用一维数组
            }
        }
        System.out.println(rec[0]);
        //System.out.println(maxSum(a,0,0));用递归的调用函数

    }
}

递归和记忆型递归都是自顶向下,dp是自底向上的,分析依赖

为了节约空间,可以使用滚动数组,改成一位诶数组

题目链接

题12:最长公共子序列

给出两个字符串,求出这样的一个最长的公共子序列的长度,而且每个字符的先后顺序和原串中的先后顺序一致,可以不相离

输入值

输入中的每行由两个由空格分隔开得字符串

输出量

每组数据,输出最大长度

样本输入

abcfbc         abfcab
programming    contest 
abcd           mnp

样本输出

4
2
0

假如S1的最后一个元素 与 S2的最后一个元素相等,那么S1和S2的LCS就等于 {S1减去最后一个元素} 与 {S2减去最后一个元素} 的 LCS 再加上 S1和S2相等的最后一个元素。
假如S1的最后一个元素 与 S2的最后一个元素不等(本例子就是属于这种情况),那么S1和S2的LCS就等于 : {S1减去最后一个元素} 与 S2 的LCS, {S2减去最后一个元素} 与 S1 的LCS 中的最大的那个序列,两者最大值。

[C[i, j]=left{egin{array}{ll} 0 & 若 i=0 或 j=0 \ C[i-1, j-1]+1 & 若 i, j>0, x_{i}=y_{j} \ max {C[i, j-1], C[i-1, j]} &若 i, j>0, x_{i} eq y_{j} end{array} ight.]

import java.util.Scanner;
public class 最长公共子序列 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        while(true){
            String s=sc.next();
            String t=sc.next();
            int sLen=s.length();
            int tLen=t.length();
            int[][] a=new int[sLen+1][tLen+1];
            for (int i = 0; i <= tLen; i++) {//初始化第一行
                a[0][i]=0;
            }
            for (int i = 0; i <= sLen; i++) {//初始化第一列
                a[i][0]=0;
            }
            for (int i = 1; i <= sLen; i++) {
                for (int j = 1; j <= tLen; j++) {
                    if(s.charAt(i-1)==t.charAt(j-1)){
                        a[i][j]=a[i-1][j-1]+1;
                    }else{
                        a[i][j]=Math.max(a[i-1][j],a[i][j-1]);
                    }
                }
            }
            System.out.println(a[sLen][tLen]);
        }
    }
}

参考博客

题目链接

题13:完全背包

有N种物品和一个容量为V的背包,每种物品都有无限件可用。第i种物品的体积是vii,价值是cii。

现在请你选取一些物品装入背包,使这些物品的体积总和不超过背包容量,且价值总和最大。

Input

第一行输出两个数N,V,分别表示物品种类数和背包容积;1≤N≤100,1≤V≤50000。 之后N行,每行两个数vii,cii,分别表示第i种物品的体积和价值;1≤vii,cii≤10000。

Output

输出一个数,表示最大的价值

Sample Input

2 11
2 3
6 14

Sample Output

20
在01背包中,我们将 j 的遍历从 sumweight从大到小,目的是为了可以在一个数组中 将(容纳i-1)和容纳i区分开来。即考虑容纳 i 个物体的时候,仅仅有选择 和 不选择两种可能。
但是在多重背包问题中,我们去可以选择多次。
转移方程应该表示成 dp[i][j] = max( dp[i-1][j],  dp[i][j-weight[i] ] + value[i] )
前一项表示,选择第i个物体的个数是0, 而第二项则表示选择了多次的i物品。 因为选择多次,重量是j的解 依赖于之前已经求出来的 dp[i][j-weight[i] 所以遍历方程确保有正确的解。
这里选择一个加上的dp[i][j-weight[i]包含选择其他的个数
import java.util.Scanner;

public class 完全背包 {
    public static void main(String[] args) {
        Scanner sc=new Scanner(System.in);
        int N=sc.nextInt();//N为物品的种类
        int V=sc.nextInt();//包的体积
        int[] volume=new int[N];
        int[] cost=new int[N];
        for (int i = 0; i < N; i++) {
            volume[i]=sc.nextInt();
            cost[i]=sc.nextInt();
        }
//        int[][] dp=new int[N+1][V+1];
//        for (int i = 1; i <=N; i++) {
//            for (int j = 1; j <=V; j++) {
//                if(j>=volume[i-1]){
//                    dp[i][j]=Math.max(dp[i-1][j],dp[i][j-volume[i-1]]+cost[i-1]);
//                }else {
//                    dp[i][j]=dp[i-1][j];
//                }
//            }
//        }
//        System.out.println(dp[N][V]);
        int[] dp=new int[V+1];
        for (int i = 0; i < N; i++) {
            for (int j =volume[i]; j <=V ; j++) {
                dp[j]=Math.max(dp[j],dp[j-volume[i]]+cost[i]);
            }
        }
        System.out.println(dp[V]);

    }
}

题目链接

参考博客

题14:最长递增子序列

给定一个无序的整数数组,找到其中最长上升子序列的长度。

示例:

输入: [10,9,2,5,3,7,101,18]
输出: 4 
解释: 最长的上升子序列是 [2,3,7,101],它的长度是 4。

说明:

  • 可能会有多种最长上升子序列的组合,你只需要输出对应的长度即可。
  • 你算法的时间复杂度应该为 O(n2) 。

所填的dp表表示为以nums[i]为结尾最长递增子序列,所有最后要遍历dp输出它的最大值

在前面找比nums[i]小的,如果存在,dp[i]为所有小于它的值dp[i]+1的最大值,如果没有一个小于它,它只能为1

public static int lengthOfLIS(int[] nums) {
    int n=nums.length;
    if(n==0){
        return 0;
    }
    int[] dp=new int[n];
    Arrays.fill(dp,1);//下面的循环不填时候的默认值
    dp[0]=1;
    for (int i=1;i<n;i++){
        for (int j = i-1; j >=0 ; j--) {
            if(nums[j]<nums[i]){//前面的数有比它小的
                dp[i]=Math.max(dp[j]+1,dp[i]);//在所有dp[i]+1中找出最大的赋值给dp[i]
            }
            //如果它前面的所有值都不小于它,只能dp[i]=1
        }
    }
    int res=1;
    for (int i = 0; i < n; i++) {
        res=Math.max(res,dp[i]);
    }
    return res;
}

题目链接

小结

  • 动态规划用于解决 多阶段 决策 最优化问题

  • 三要素

    • 阶段

    • 状态

    • 决策

  • 两个条件:

    • 最优子结构(最优化原理) 当前状态的决策依赖于历史上以前子问题决定

    • 无后效性:当前状态是前面状态的完美总结 后面的状态由前面决定,现在的状态不会影响历史状态

  • 是否可以用动态规划,否则用搜索。

    • 模型匹配:多做题,掌握经典模型
      一维:上升子序列模型,背包模型
      二维:最长公共子序列问题.

    • 寻找规律:规模由小到大,或者由大到小,做逐步分析

    • 放宽条件或增加条件

  • 一般过程

    • 找到过程演变中变化的量(状态),以及变化的规律(状态转移方程)

    • 确定一些初始状态,通常需要dp数组来保存

    • 利用状态转移方程,推出最终答案

  • 解法

    • 自顶向下,记录递归;如果有重叠子问题,带备忘录.

    • 自底向上,递推

贪心和动规

  • 可以用局部最优解来推导全局最优解,即动态规划

  • 贪心:这一阶段的解,由上一阶段直接推导出

  • 动规:当前问题的最优解,不能从上一阶段子问题简单得出前面多阶段多层子问题共同计算出,因此需要保留历史上求解过的子问题及其最优解

原文地址:https://www.cnblogs.com/cxynb/p/12527697.html