算法与数据结构基础课第九节笔记

贪心算法

  1. 最自然智慧的算法
  2. 用一种局部最功利的标准,总是做出在当前看来是最好的选择
  3. 难点在于证明局部最功利的标准可以得到全局最优解
  4. 对于贪心算法的学习主要以增加阅历和经验为主

例1:给定一个由字符串组成的数组strs,必须把所有的字符串拼接起来,返回所有可能的拼接结果中,字典序最小的结果

贪心策略1:

先给数组按照字典序排序,然后依次拼起来,适用于大多数case,但是个别如:ba,b 会拼成bba,但是bab实际上字典序更小,也就是

两个字符串x,y

x 的字典序 小于等于 y 的,那么x 放在前面,否则,y放前面

改进:

x 拼上 y 小于等于 y拼上x ,那么 x 放前面,否则, y 放前面

全排列枚举:

       public static String lowestString1(String[] strs) {
		if (strs == null || strs.length == 0) {
			return "";
		}
		ArrayList<String> all = new ArrayList<>();
		HashSet<Integer> use = new HashSet<>();
		process(strs, use, "", all);
		String lowest = all.get(0);
		for (int i = 1; i < all.size(); i++) {
			if (all.get(i).compareTo(lowest) < 0) {
				lowest = all.get(i);
			}
		}
		return lowest;
	}

	// strs 里放着所有的字符串
	// 已经使用过的字符串的下标,在use里登记了,不要再使用了
	// 之前使用过的字符串,拼接成了 path
	// 用all 收集所有可能的拼接结果
	public static void process(String[] strs, HashSet<Integer> use, String path, ArrayList<String> all) {
		if (use.size() == strs.length) {
			all.add(path);
		} else {
			for (int i = 0; i < strs.length; i++) {
				if (!use.contains(i)) {
					use.add(i);
					process(strs, use, path + strs[i], all);
					use.remove(i);
				}
			}
		}
	}

贪心算法:

       public static class MyComparator implements Comparator<String> {
		@Override
		public int compare(String a, String b) {
			return (a + b).compareTo(b + a);
		}
	}

	public static String lowestString2(String[] strs) {
		if (strs == null || strs.length == 0) {
			return "";
		}
		Arrays.sort(strs, new MyComparator());
		String res = "";
		for (int i = 0; i < strs.length; i++) {
			res += strs[i];
		}
		return res;
	}

贪心算法求解的标准过程

1,分析业务

2,根据业务逻辑找到不同的贪心策略

3,对于能举出反例的策略直接跳过,不能举出反例的策略要证明有效性

这往往是特别困难的,要求数学能力很高且不具有统一的技巧性

贪心算法的解题套路

1,实现一个不依靠贪心策略的解法X,可以用最暴力的尝试

2,脑补出贪心策略A、贪心策略B、贪心策略C...

3,用解法X和对数器,用实验的方式得知哪个贪心策略正确

4,不要去纠结贪心策略的证明

贪心算法的解题套路实战

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

按照会议结束时间早安排

        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 bestArrange1(Program[] programs) {
		if (programs == null || programs.length == 0) {
			return 0;
		}
		return process(programs, 0, 0);
	}

	/**
	 * 还剩什么会议都放进programs
	 * done 之前已经安排了多少会议,数量
	 * timeLine目前来到的时间点是什么
	 *
	 * 目前来到timeLine的时间点,已经安排了done多的会议,剩下的会议programs可以自由安排
	 * 返回能安排的最多会议数量
	 * @param programs
	 * @param done
	 * @param timeLine
	 * @return
	 */
	public static int process(Program[] programs, int 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;
	}

	public static Program[] copyButExcept(Program[] programs, int i) {
		Program[] ans = new Program[programs.length - 1];
		int index = 0;
		for (int k = 0; k < programs.length; k++) {
			if (k != i) {
				ans[index++] = programs[k];
			}
		}
		return ans;
	}

贪心:

      public static int bestArrange2(Program[] programs) {
		Arrays.sort(programs, new ProgramComparator());
		int timeLine = 0;
		int result = 0;
		for (int i = 0; i < programs.length; i++) {
			if (timeLine <= programs[i].start) {
				result++;
				timeLine = programs[i].end;
			}
		}
		return result;
	}

	public static class ProgramComparator implements Comparator<Program> {

		@Override
		public int compare(Program o1, Program o2) {
			return o1.end - o2.end;
		}

	}

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

暴力解法:

       public static int minLight1(String road) {
		if (road == null || road.length() == 0) {
			return 0;
		}
		return process(road.toCharArray(), 0, new HashSet<>());
	}

	/**
	 * str[index ...] 位置,自由选择放灯还是不放
	 * str[0...index-1] 位置,已经做完决定了,那些放了灯的位置,放在lights里
	 * 要求选出能照亮所有点的方案,并且在这些方案中,返回最少需要几个灯
	 * @param str
	 * @param index
	 * @param lights
	 * @return
	 */
	public static int process(char[] str, int index, HashSet<Integer> lights) {
		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 lights.size();
		} else {
			// 不放
			int no = process(str, index + 1, lights);
			int yes = Integer.MAX_VALUE;
			// 是点才能放
			if (str[index] == '.') {
				lights.add(index);
				yes = process(str, index + 1, lights);
				lights.remove(index);
			}
			return Math.min(no, yes);
		}
	}

贪心算法:

      public static int minLight2(String road) {
		char[] str = road.toCharArray();
		int index = 0;
		int light = 0;
		while (index < str.length) {
			if (str[index] == 'X') {
				index++;
			} else {
				// 必然要放一个灯
				light++;
				if (index + 1 == str.length) {
					break;
				} else {
					// 下一个位置是X,就跳到下下一个位置
					if (str[index + 1] == 'X') {
						index = index + 2;
					} else {
						// 不是X,有两种情况,iii,iix 那么也是需要一个灯就行,只是iii放在中间,iix放在前面和中间都行
						// 所以要走三步,i不会被之前位影响
						index = index + 3;
					}
				}
			}
		}
		return light;
	}

例4:

一块金条切成两半,是需要花费和长度数值一样的铜板的。
比如长度为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铜板。
输入一个数组,返回分割的最小代价。

哈夫曼树

暴力解法:

        public static int lessMoney1(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++) {
				ans = Math.min(ans, process(copyAndMergeTwo(arr, i, j), 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;
	}

贪心:

      public static int lessMoney2(int[] arr) {
		PriorityQueue<Integer> pQ = new PriorityQueue<>();
		for (int i = 0; i < arr.length; i++) {
			pQ.add(arr[i]);
		}
		int sum = 0;
		int cur = 0;
		while (pQ.size() > 1) {
			cur = pQ.poll() + pQ.poll();
			sum += cur;
			pQ.add(cur);
		}
		return sum;
	}

例5:

输入: 正数数组costs、正数数组profits、正数K、正数M
costs[i]表示i号项目的花费
profits[i]表示i号项目在扣除花费之后还能挣到的钱(利润)
K表示你只能串行的最多做k个项目
M表示你初始的资金
说明: 每做完一个项目,马上获得的收益,可以支持你去做下一个项目。不能并行的做项目。
输出:你最后获得的最大钱数。

       public static int findMaximizedCapital(int K, int W, int[] Profits, int[] Capital) {
		PriorityQueue<Program> minCostQ = new PriorityQueue<>(new MinCostComparator());
		PriorityQueue<Program> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
		// 先根据花费组织小根堆
		for (int i = 0; i < Profits.length; i++) {
			minCostQ.add(new Program(Profits[i], Capital[i]));
		}
		for (int i = 0; i < K; i++) {
			// 将初始资金能支持的项目,放入利润的大根堆
			while (!minCostQ.isEmpty() && minCostQ.peek().c <= W) {
				maxProfitQ.add(minCostQ.poll());
			}
			// K个数量没做够,但是项目做完了,或者启动资金不够解锁项目
			if (maxProfitQ.isEmpty()) {
				return W;
			}
			// 做完一个项目,初始资金加利润,就是下次的启动资金
			W += maxProfitQ.poll().p;
		}
		return W;
	}

	public static class Program {
		public int p;
		public int c;

		public Program(int p, int c) {
			this.p = p;
			this.c = c;
		}
	}

	public static class MinCostComparator implements Comparator<Program> {

		@Override
		public int compare(Program o1, Program o2) {
			return o1.c - o2.c;
		}

	}

	public static class MaxProfitComparator implements Comparator<Program> {

		@Override
		public int compare(Program o1, Program o2) {
			return o2.p - o1.p;
		}

	}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值