左神算法:前缀树和贪心

1.前缀树

某些字符串在生成这些前缀树的过程中是这么生成的。前缀树也是一颗树结构假设把“abc”放入这颗树中。看头结点有没有走向a的路,每一个字母是填在路上的,不是填在点上面的。没有就加入,通过这种方式把abc加入到树中。如果把“abd”加到树上,从头结点开始有走向a的路吗?有,就复用,不重复建。再看从a到达的结点往后有没有b的路,有就复用,但是之后没有走向d的路。就建出来。一个字符串加的过程中总是从头结点开始,依次看有没有沿途的路,如果有,复用,如果没有就建出来。干净的结点的话:可以用来判断这些字符串以什么开头。增加功能:看字符串中有没有“be”,只能知道树中有没有“be”并不知道“be”出现了几次。所以在结点上增加一个数据项,这个数据项的功能是看有多少个字符串是以当前的结点结尾的。把某一个数据项加到结点上,让这个结点本身的值的内容丰富起来之后,那么这个前缀树的功能就可以扩充了。
扩充功能,就是丰富结点的数据项,与前缀有关的。
加每个字符串的代价就是字符串的长度,查的时候也是。和样本量无关和样本的长度有关。

前缀树创建的示意图
在这里插入图片描述

public class Code_01_TrieTree {
public static class TrieNode{
  private int end;//有多少东西以他结尾
  private int path;//有多少东西达到过它
  private TrieNode[] nexts;//我们用检查某条路上的结点是不是空,来标记这条路存在不存在。
  
  //也可以做成这种map形式的,字符和结点对应的形式。
  private HashMap<Character, TrieNode> map;
  
  public TrieNode(){
   end = 0;
   path = 0;
   nexts = new TrieNode[26];//路每一个结点都有26条路,我们强行规定这里只存26个小写英文字母。
  }
  public static class Trie{
  private TrieNode root;//头结点
  
  public Trie(){
   root = new TrieNode();
  }
  public void insert(String word) {
   int index = 0;
   TrieNode node = root;
   char[] charArray = word.toCharArray();
   for (int i = 0; i < charArray.length; i++) {
    index = charArray[i] - 'a';//用于计算char数组里面的元素是什么
    if(node.nexts[index] == null) {//当前node有没有走向当前字母的路,
     node.nexts[index] = new TrieNode();//如果没有建出来
    }
    node = node.nexts[index];
    node.path++;
   }
   node.end++;
  }
public void delete(String word) {
   if(search(word) == 0) {
    return ;
   }
   char[] charArray = word.toCharArray();
   int index = 0;
   TrieNode node = root;
   for (int i = 0; i < charArray.length; i++) {
    index = charArray[i] - 'a';
    if(--node.nexts[index].path == 0) {
     node.nexts[index] = null;
     return ;
    }
    node = node.nexts[index];
   }
   node.end--;
  }
  //插入字符串的次数
  public int search(String word) {
   if(word == null) {
    return 0;
   }
   TrieNode node = root;
   char[] charArray = word.toCharArray();
   int index = 0;
   for (int i = 0; i < charArray.length; i++) {
    index = charArray[i] - 'a';
    if(node.nexts[index] == null) {
     return 0;
    }
    node = node.nexts[index];
   }
   return node.end;
  }
 }

2.贪心

贪心策略的正确性的证明,千万别去纠结。怎么验证正确呢?写对数器来验证。在实际的面试中,脑补了十七八个贪心策略都实现了不知道哪个对,写一个暴力的方法,不用贪心的,绝对对的。用对数器小样本的验,随机的验,他对了就可以去过oj。在小样本中对数器都对,那么就认为他在大样本中也对。什么是贪心:你定一个指标,在这个指标下,把每一个样本分出一个优先来,按照优先大的先执行,优先小的后执行。贪心就是某一种简洁的标准,在这个标准下给所有的东西分个1,2,3出来,然后根据排的顺序,或者是我定一个优先级决定一种顺序,就是贪心。

贪心的策略靠积累

  1. 给定一个字符串类型的数组strs,找到一种拼接方式,使得把所 有字 符串拼起来之后形成的字符串具有最低的字典序。

贪心策略:如果只有小写字母的时候,把字符串看成是26进制的数,当字符串长度一样时,就是字母值的大小比较。
如果长度不等时,把短的后面补上ascall码为0的值让他变成长度一样长的时候再比。比如“abc”和“b”比,把“b”变成“b00”.在比。
不可以按照字符串的字典序排好后拼接起来。因为“b”会在“ba”前面,但是“bba”>“bab”。所以这种的贪心策略不对。
可以使用,如果str1+str2<=str2+str1就把str1放前面,否则把str2放前面。排序策略的本身要有传递性。有些题里面贪心策略不是比较策略。需要证明比较策略有传递性。如果把字符串理解为k进制的数,所谓一个字符串贴上一字符串,就是前一个字符串向左位移了b的长度这么多位,然后再加上b这个值。a+b等同于a做高位加上b做低位。“abc”和“efg”,“abc”向左位移3位加上“efg”。

本题的贪心策略证明:

在这里插入图片描述

//自定义的比较器
public static class myComparator implements Comparator<String>{
@Override
  public int compare(String o1, String o2) {
   // TODO Auto-generated method stub
   return (o1+o2).compareTo(o2+o1);
  }
 }
 
 public static String lowestString(String[] strs) {
  if(strs.length == 0 && strs == null) {
   return null;
  }
  Arrays.sort(strs, new myComparator());
  String res = "";
  for (int i = 0; i < strs.length; i++) {
   res += strs[i];
  }
  return res;
 }
  1. 切金条问题:
    一块金条切成两半,是需要花费和长度数值一样的铜板的。比如 长度为20的 金条,不管切成长度多大的两半,都要花费20个铜 板。一群人想整分整块金 条,怎么分最省铜板? 例如,给定数组{10,20,30},代表一共三个人,整块金条长度为 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 class Code_02_LessMoney {
public static int lessMoney(int[] arr) {
//优先级队列不使用比较器的话,默认创建的是小根堆
  PriorityQueue<Integer> minHeap = new PriorityQueue<>();
  for (int i = 0; i < arr.length; i++) {
   minHeap.add(arr[i]);
  }
  int sum = 0;
  int cur = 0;
  while(minHeap.size() > 1) {//这里不能写!minHeap.isEmpty(),因为当队列中只有一个数的时候,弹出一个数后就空了会报空指针异常
   cur = minHeap.poll() + minHeap.poll();
   sum += cur;
   minHeap.add(cur);
  }
  return sum;
 }

public static class MinheapComparator implements Comparator<Integer>{
@Override
  //小根堆暂时理解这个比较器 最后打印值的时候,小根堆是一个升序的效果
  /*
   * 返回值为正数的时候,o2放前面
   * 返回值为负数的时候,o1放前面
   * 返回值为0,证明两数相等
   */
  public int compare(Integer o1, Integer o2) {
   // TODO Auto-generated method stub
   return o1 - o2;
  }
  }
  public static class MaxheapComparator implements Comparator<Integer>{
  @Override
  public int compare(Integer o1, Integer o2) {
   // TODO Auto-generated method stub
   return o2 - o1;
  }
  }

public static void main(String[] args) {
int[] arr = { 6, 7, 8, 9 };
  System.out.println(lessMoney(arr));
  }
  }
  1. IPO问题
    输入: 参数1,正数数组costs 参数2,正数数组profits 参数3, 正数k 参数4,正数m costs[i]表示i号项目的花费 profits[i]表示i号项目在扣除花 费之后还能挣到的钱(利润) k表示你不能并行、只能串行的最多 做k个项目 m表示你初始的资金 说明:你每做完一个项目,马上获得的收益,可以支持你去做下 一个 项目。 输出: 你最后获得的最大钱数。

每个项目会有两个信息,一个是你做这个项目的需要花多少钱,一个是你做了这个项目后能获得多少利润。可以利用启动资金去做项目,可以做花费小于初始资金的项目,但一次只能做一个。最多可以做k个项目。准备一个小根堆,这个小根堆是按照花费,谁低谁放到小根堆的头部,将小根堆里面比花费低的全部弹出来,弹到一个大根堆里面,这个大根堆是按照收益高,谁收益高谁放在大根堆的头部。小根堆是所有的项目,大根堆里面是当前的资金可以解锁的项目,每次做大根堆里面的头部,然后初始资金增加,再看小根堆里面哪些项目可以解锁,可以就把他放在大根堆里面。就这样一直做k个结束。当大根堆为空或k次执行完,结束。

//这个类模拟是项目
	public static class Node{
		private int p;//项目的收益
		private int c;//项目的花费
		
		public Node(int p,int c) {
			this.p = p;
			this.c = c;
		}
	}
	
	public static class minCostComparator implements Comparator<Node>{

		@Override
		public int compare(Node o1, Node o2) {
			// TODO Auto-generated method stub
			return o1.c - o2.c;
		}
		
	}
	
	public static class maxprofitComparator implements Comparator<Node>{

		@Override
		public int compare(Node o1, Node o2) {
			// TODO Auto-generated method stub
			return o2.p - o1.p;
		}
		
	}
	
	//
	public static int findMaximizedCapital(int w,int k,int[] profits,int[] costs) {
		Node[] node = new Node[profits.length];
		for (int i = 0; i < node.length; i++) {
			node[i] = new Node(profits[i], costs[i]);
		}
		PriorityQueue<Node> minCost = new PriorityQueue<Node>(new minCostComparator());
		PriorityQueue<Node> maxProfit = new PriorityQueue<Node>(new maxprofitComparator());
		for (int i = 0; i < node.length; i++) {
			minCost.add(node[i]);
		}
		for (int i = 0; i < k; i++) {
			while(!minCost.isEmpty() && w >= minCost.peek().c) {
				maxProfit.add(minCost.poll());
			}
			if(maxProfit.isEmpty()) {
				return w;
			}
			w += maxProfit.poll().p;
		}
		return w;
	}
  1. 会议室问题:
    一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目 的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数 组,里面 是一个个具体的项目),你来安排宣讲的日程,要求会 议室进行 的宣讲的场次最多。返回这个最多的宣讲场次。

有几种贪心策略呢。按项目哪个早开始就安排他,不可以,因为如果你个项目早开始,占全天,不行的;按照哪个的持续时间短,也贪不到正确的,因为可以有其中的一个持续时间短,但他正好夹在两个的中间,也不行。按照早结束的贪是可以找出最优解的。不断的推进项目,每一个项目完成就把时间推到这个项目的结束时间再继续遍历

//项目结点
	public static class Node{
		private int start;
		private int end;
		
		public Node(int start,int end) {
			this.start = start;
			this.end = end;
		}
	}
	
	public static class endMinComparator implements Comparator<Node>{

		@Override
		public int compare(Node o1, Node o2) {
			// TODO Auto-generated method stub
			return o1.end - o2.end;
		}
		
	}
	
	//cur 是指当前时间点
	public static int bestArrange(Node[] arr,int cur) {
		Arrays.sort(arr, new endMinComparator());
		int result = 0;
		for (int i = 0; i < arr.length; i++) {
			if(cur <= arr[i].start) {
				result++ ;
				cur = arr[i].end;
			}
		}
		return result;
	}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值