贪心算法

概念:

贪心算法(greedy algorithm [8] ,又称贪婪算法)是指,在对问题求解时,总是做出在当前看来是最好的选择。也就是说,不从整体最优上加以考虑,算法得到的是在某种意义上的局部最优解 [1] 。

题目:

字典序拼接字符串

题目描述:字符串数组,将各字符串拼接,使得最低词典顺序。如:“ab”,“cd”,“ef”,拼接成“abcdef”最小,其他方式都比它大。

思路:

  1. 首先按照首字母最小的顺序排序,b,ba,1,10不满足条件

  2. 两个字符串相加,谁小谁在前面的原则

    1. 直接用Array.sort,进行排序
    2. 直接用Priority队列
public static String lowestString1(String[] strs){
    if(strs==null || str.length == 0){
        return "";
    }
    Arrays.sort((strs,(o1,o2)-> (o1+o2).compareTo(o2+o1)));
    StringBuffer str = new StringBuffer();
    for(String s : strs){
        strs.append(s);
    }
    return str.toString();
}

暴力递归:

思路:

  1. 对每个字符串都进行拼接,把所有可能的字符串放入TreeSet(Priority)中
  2. 首先先对字符串集合做一个循环,然后每一个循环的字符串后,删除该集合中的字符串,放入下一个该程序中,然后接着重复递归,直到没有该集合,则直接返回"",然后对返回的set做一个拼接
public static String lowestString2(String[] strs){
    if(strs == null || strs.length == 0){
        return "";
    }
    TreeSet<String> ans = process(strs);
    return ans.pollFirst();
}

public static TreeSet<String> process(String[] strs){
    TreeSet<String> ans = new TreeSet<>();
    if(strs.length == 0){
        //当前传入的数组长度为0,说明没有数组,
        ans.add("");
        return ans;
    }
    for(int i = 0;i < strs.length;i++){
        String first = strs[i];
        //返回删除当前字段的集合
        String[] nexts = removeIndexString(strs,i);
        //这个返回的是当前元素除外,所有后面元素拼接的总量set
        TreeSet<String> next = process(nexts);
        //然后和他进行拼接
        for(String cur : next){
            ans.add(first + cur);
        }
    }
    return ans;
}

public static String[] removeIndexString(String[] arr,int index){
    int N = arr.length;
    String[] ans = new String[N-1];
    int ansIndex = 0;
    for(int i = 0;i < N;i++){
        if(i != index){
            ans[ansIndex++] = arr[i];
        }
    }
    return ans;
}

public static String[] generateRandomStringArr(int arrLen,int strLen){
    String[] strArr = new String[(int)Math.random()*arrLen];
    for(int i = 0;i < strArr.length;i++){
        strArr[i] = generateRandomString(strLen);
    }
    return strArr;
}

public static String generateRandomString(int strLen){
    char[] ans = new char[(int) (Math.random()*strLen) + 1];
    for(int i = 0;i < ans.length;i++){
        int value = (int) (Math.random() * 10);
        ans[i] = Math.random() <= 0.5?(char)('a' + value) : (char)('A' + value);
    }
    return String.valueOf(ans);
}

public static String[] copyStringArr(String[] arr){
    String[] ans = new String[arr.length];
    for(int i = 0;i < ans.length;i++){
        ans[i] = String.valueOf(arr[i]);
    }
    return ans;
}

public static void main(String[] args){
    int testTimes = 10000;
    int strLen = 10;
    int arrLen = 10;
    for(int i = 0;i < testTimes; i++){
        String[] test1 = generateRandomStringArr(arrLen,strLen);
        String[] test2 = copyStringArr(test1);
        String ans1 = lowerString1(test1);
        String ans2 = lowerString2(test2);
        if(!ans1.equals(ans2)){
            System.out.println("Oops");
        }
        System.out.println("finish");
    }
}

点灯问题

题目:给定一个字符串str,只由‘X’和‘.’两种字符构成。‘X’表示墙,不能放灯,也不需要点亮。‘.’表示居民点,可以放灯,需要点亮。如果灯放在i位置,可以让i-1,i和i+1三个位置被点亮。返回如果点亮str中所有需要点亮的位置,至少需要几盏灯。

贪心:

思路:

  1. X是墙,不需要放灯,.表示居民,可以放灯,也可以不放灯,但是所有的居民点必须被点亮

  2. 对当前字符串进行循环,i位置是x,跳过

  3. 当前i位置是.

    1. .i+1位置是x,则i位置点灯
    2. i+1位置是.则判断i+2位置
    3. 如果i+2位置是.,则i+1位置点灯,跳到i+3
    4. 如果i+2位置是x,则i+1位置点灯,跳到i+3、
  4. 因此,只要i+1位置是.,则直接在i+1位置点灯,ans+1,然后跳到i+3;只要i+1位置是;只要i位置是x,跳到i+1位置

    public static int minLight1(String road){
        int ans = 0;
        int i = 0;
        char[] roadArr = road.toCharArray();
        while(i < roadArr.length){
            //1.当前i位置是x
            if(roadArr[i] == 'x'){
                i++;
            } else {
                ans++;
                //如果当前i到了尽头,则直接退出当前循环
                if(i + 1 == roadArr.length){
                    break;
                } else {
                    //当前i位置是.
                    //当前i+1位置是x,则i位置点灯,同时跳到i+2位置
                    if(radArr[i + 1] == 'x'){
                        i = i + 2;
                    } else {
                        i = i + 3;
                    }
                }
            }
        }
        return ans;
    }
    
    

    暴力递归:

    思路:

    1. hashSet记录当前灯点亮的位置,index记录当前灯的位置放入hashSet中
    public static int minLight2(String road){
        if(road == null || road.length() == 0){
            return 0;
        }
        return process(road.toCharArray(),0,new HashSet<>());
    }
    
    public static int process(char[] str,int index,HashSet<Integer> lights){
        //当前遍历的位置到了str的末尾
        if(index == str.length){
            for(int i = 0;i < str.length; i++){
                if(str[i] != 'x'){
                    //判断当前点灯情况是否满足全部居民亮的情况,不满足,返回最大值
                	if(!lights.contains(i-1)&&!lights.contains(i)&&!lights.contains(i+1)){
                    	return Integer.MAX_VALUE;
                	}
                }
            }
            return ligth.size();
        } else {
            //no 表示当前位置没有点灯
            int no = process(str,index + 1,lights);
            //yes有两种情况,一种情况是墙,则直接赋最大值
            int yes = Integer.MAX_VALUE;
            //第二种是.,则lights添加灯,然后传入process
            if(str[index] == '.'){
                lights.add(index);
                yes = process(str,index+1,lights);
                lights.remove(index);
            }
            return Math.min(yes,no);
        }
    }
    
    

    公式:

    思路:

    1. 两个x之间,数一下.的数量,然后除以3,向上取整
    public static int minLight3(String road){
        char[] str = road.toCharArray();
        //cur 表示当前两个x之间的数量
        int cur = 0;
        int light = 0;
        for(char c : str){
            //遇到第一个x结束cur计数
            //同时统计当前需要点灯的数量,cur+2是为了向上取整
            if(c == 'x'){
                light += (cur+2)/3;
                cur = 0;
            } else {
                cur++;
            }
        }
        //最后统计到末尾的情况
        light += (cur+2)/3;
        return ligth;
    }
    
    public static String generateRandomString(int len){
        char[] charArr = new char[(int)(Math.random() * len)];
        for(int i = 0;i<charArr.length;i++){
            charArr[i] = Math.random() < 0.5 ? 'x' : '.';
        }
        return String.valueOf(charArr);
    }
    
    public static void main(String[] args){
        int testTimes = 1000;
        int len = 10;
        for(int i = 0;i < testTimes;i++){
            String test = generateRandomString(len);
            int a1 = minLight1(test);
            int a2 = minLight2(test);
            int a3 = minLight3(test);
            if(a1 != a2 || a2 != a3){
                System.out.println("Oops");
                System.out.println(test);
            }
        }
        System.out.println("finish");
    }
    

分割金条

题目: 一块金条切成两半,是需要花费和长度数值一样的铜板,比如长度为20的金条,不管怎么切都要花费20个铜板,一群人想整分整块金条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为60,金条要分成10,20,30三个部分。 如果先把长度60的金条分成10和50,花费60;再把长度50的金条分成20和30,花费50;一共花费110铜板 但如果先把长度60的金条分成30和30,花费60;再把长度30金条分成10和20,花费30;一共花费90铜板 输入一个数组,返回分割的最小代价

贪心:

思路:

  1. 给定的数组每次把最小的两个数合成一个数,然后放入队列中,继续合成,直到当前队列中只有一个数,然后返回合成过程中的答案
public static int lessMoney1(int[] arr){
    PriorityQueue<Integer> pq = new Priority<Queue>();
    for(int i : arr){
        pq.add(i);
    }
    int ans = 0;
    int p1 = 0;
    int p2 = 0;
    while(pq.size() > 1){
        p1 = pq.poll();
        p2 = pq.poll();
        ans += p1+p2;
        pq.add(p1+p2);
    }
    return ans;
}

递归:

思路:

  1. 每种可能都尝试一边
public static int lessMoney2(int[] arr){
    if(arr == null||arr.length == 0){
        return 0;
    }
    return process(arr,0);
}

public static int process(int[] arr,int pre){
    if(arr.length == 1){
        return pre;
    }
    int ans = Integer.MAX_VALUE;
    for(int i = 0;i< arr.length;i++){
        for(int j = i+1;j<arr.length;j++){
            int[] newArr = copyAndMergeTwo(arr,i,j);
            ans = Math.min(ans,process(newArr,pre+arr[i] + arr[j]));
        }
    }
    return ans;
}

public static int[] copyAndMergeTwo(int[] arr,int i,int j){
    int[] ans = new int[arr.length-1];
    int ansi = 0;
    for(int arri=0;arri<arr.length;arri++){
        if(arri != i && arri != j){
            ans[ansi++] = arr[arri];
        }
    }
    ans[ansi] = arr[i] + arr[j];
    return ans;
}

会议室宣讲

题目:

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲,给你每一个项目开始的时间和结束的时间,你来安排宣讲的日程,要求会议室进行的宣讲的场次最多,返回最多的宣讲场次

贪心:

思路:

  1. 按每个项目的结束时间做排序,每次都挑选能最早结束的时间会议,返回这个会议数量
public static class Program{
    public int start;
    public int end;
    
    public Program(int start,int end){
        this.start = start;
        this.end = end;
    }
}

public static int bestArrange2(Program[] programs){
    Arrays.sort(programs,(o1,o2) -> o1.end - o2.end);
    int ans = 0;
    int timeLine = 0;
    for(int i = 0;i<programs.length;i++){
        if(timeLine <= programs[i].start){
            ans++;
            timeLine = programs[i].end;
        }
    }
    return ans;
}

递归:

思路:

  1. 遍历每个会议,找出最小值
public static int bestArrange3(Program[] programs){
    if(programs.length == 0|| programs == null){
        return 0;
    }
    return process(programs,0,0);
}

public static int process(Program[] programs,intt done,int timeLine){
    if(programs.length == 0){
        return done;
    }
    int max = done;
    for(int i = 0;i<programs.length;i++){
        if(programs[i].start >= timeLine){
            Program[] next = copyButExcept(programs,i);
            max = Math.max(max,process(next,done+1,programs[i].end));
        }
    }
    return max;
}

项目收益

题目:

输入:正数数组costs正数数组profits正数k

正数k含义:能串行的最多做k个项目

costs[i] 表示 i 号项目的花费

profits[i] 表示 i 号项目在扣除花费之后还能挣到的钱(利润)

w表示你初始的资金

说明:

你每做完一个项目,马上获得的收益,可以支持你去做下一个项目。

输出:你最后获得的最大钱数。

贪心:

思路:

  1. 每次都选择能做项目当中的最大收益的项目,然后要么k次做完,要么本金不够,最后返回最大钱数
  2. 可以用两个队列,一个队列放最小的花费的项目minCost,一个队列存放最大的利润
public static class Program{
    public int p;
    public int c;
    
    public Program(int p , int c){
        this.p = p;
        this.c = c;
    }
}

public static int findMaximizedCapital(int K,int W,int[] profits,int[] Capital){
    PriorityQueue<Program> minCost = new Priority<>((o1,o2) -> o1.c-o2.c);
    PriorityQueue<Program> maxPro = new Priority<>((o1,o2) -> o2.p - o1.p);
    //1.minCost塞满
    for(int i = 0;i < Profits.length;i++){
        minCost.add(new Program(Profits[i],Capital[i]));
    }
    //对k次项目做循环
    for(int i = 0;i < K;i++){
        //往mmaxPro里放满足条件能够花费的项目
        while(!minCost.isEmpty() minCost.peek().c <= W){
            maxPro.add(minCost.poll());
        }
        //表示当前项目没有能够消费的
        if(maxPro.isEmpty()){
            break;
        }
        W += maxPro.poll().p;
    }
    return W;
}