因为甲流耽搁了几天,今天继续记录。
一、前缀树
-
什么是前缀树?
Trie树,即字典树,又称单词查找树或键树,是一种树形结构,是一种哈希树的变种。
典型应用是用于统计和排序大量的字符串(但不仅限于字符串),所以经常被搜索引擎系统用于文本词频统计。
它的优点是:利用字符串的公共前缀来减少查询时间,最大限度地减少无谓的字符串比较。 -
前缀树的3个基本性质:
- 根节点不包含字符,除根节点外每一个节点都只包含一个字符。
- 从根节点到某一节点,路径上经过的字符连接起来,为该节点对应的字符串。
- 每个节点的所有子节点包含的字符都不相同。
【例题】一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。
public class TrieTree {
/**
* 前缀树节点
*/
public static class TrieNode {
// 单词经过该节点的次数,以该节点之前字符为前缀的字符数量
public int pass;
// 以该节点为结尾的单词个数
public int end;
// 当字符种类多于26个字母 HashMap<char, TrieNode>或TreeMap
public TrieNode[] nexsts;
public TrieNode() {
pass = 0;
end = 0;
/*
nexts[0] == null 没有走向‘a’的路
nexts[0] != null 有走向‘a’的路
···
nexts[25] != null 有走向‘z’的路
*/
nexsts = new TrieNode[26];
}
}
/**
* 前缀树类
*/
public static class Trie {
private TrieNode root;
public Trie() {
root = new TrieNode();
}
/**
* 添加单词
*/
public void insert(String word) {
if (word == null) {
return;
}
char[] chs = word.toCharArray();
// 从根节点出发
TrieNode node = root;
node.pass++;
int index = 0;
for (int i = 0; i < chs.length; i++) {
index = chs[i] - 'a';
if (node.nexsts[index] == null) {
// ‘a’字符位置为null,说明还没有前缀为a的路,需要新建出来
node.nexsts[index] = new TrieNode();
}
node = node.nexsts[index];
node.pass++;
}
node.end++;
}
/**
* 查询word这个单词之前加入过几次
* 就是每个节点end的含义
*/
public int search(String word) {
if (word == null) {
return 0;
}
char[] chars = word.toCharArray();
TrieNode node = root;
int index = 0;
for (char curChar : chars) {
index = curChar - 'a';
if (node.nexsts[index] == null) {
return 0;
}
node = node.nexsts[index];
}
return node.end;
}
/**
* 所有加入的字符串中,有几个是以pre这个字符串作为前缀的
* 就是每个节点pass的含义
*/
public int prefixNumber(String pre) {
if (pre == null) {
return 0;
}
char[] chars = pre.toCharArray();
TrieNode node = root;
int index = 0;
for (char curChar : chars) {
index = curChar - 'a';
if (node.nexsts[index] == null) {
return 0;
}
node = node.nexsts[index];
}
return node.pass;
}
/**
* 删除单词word
* 需要对沿途节点的pass修改,同时修改终点的end
*/
public void delete(String word) {
// 先进行判断当前树中是否有word
if (search(word) != 0) {
char[] chars = word.toCharArray();
TrieNode node = root;
int index = 0;
for (char curChar : chars) {
index = curChar - 'a';
if (--node.nexsts[index].pass == 0) {
// java可以这样释放空间,但C++需要遍历到底手动释放
node.nexsts[index] = null;
return;
}
node = node.nexsts[index];
}
node.end--;
}
}
}
}
二、贪心算法
在某一个标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫作贪心算法。
也就是说,不从整体最优上加以考虑,所做出的是在某种意义上的局部最优解。
-
关键是判断为 “优” 的标准
-
左神说,目前贪心算法的题无法准确判断出哪个是最优标准,只能利用对数器一个一个试,好在贪心算法的代码很简洁,也很好写,明确了判定标准后,可以很快写出。
- 不过想我这种菜鸡🐔连一个标准都可能想不出来 _(:3」∠)_
贪心算法的在笔试时的解题套路
- 实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
- 脑补出贪心策略A、贪心策略B、贪心策略C…
- 用解法X和对数器,去验证每一个贪心策略,用实验的方式得知哪个贪心策略正确
- 不要去纠结贪心策略的证明
对数器的补充
对数器的概念和使用
- 有一个你想要测的方法a
- 实现复杂度不好但是容易实现的方法b
- 实现一个随机样本产生器
- 把方法a和方法b跑相同的随机样本,看看得到的结果是否一样。
- 如果有一个随机样本使得比对结果不一致,打印样本进行人工干预,改对方法a或者方法b
- 当样本数量很多时比对测试依然正确,可以确定方法a已经正确。
下面题目二有对数器案例
题目一:最多可以参加的会议数量
一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。
给你每一个项目开始的时间和结束的时间[startTime, endTime]
(给你一个数组,里面是一个个具体的项目),你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。返回这个最多的宣讲场次。
- 解题思路:首先要选择先安排的会议(最优的会议),什么样的会议优先安排?有多个标准:开始时间最早的会议、会议持续时间最短的会议、结束时间最短的会议······等等。
这道题应该选择的标准为:结束时间最短的会议。如何证明?利用数学方法。
public class MeetingArrange {
public static class Program {
public int start;
public int end;
public Program(int start, int end) {
this.start = start;
this.end = end;
}
}
public static class ProgramComparator implements Comparator<Program> {
@Override
public int compare(Program o1, Program o2) {
return o1.end - o2.end;
}
}
/**
* 会议安排
* @param programs 所有的会议
* @param timePoint 开始的时间
* @return 所能安排的会议的最大数量
*/
public static int bestArrange(Program[] programs, int timePoint) {
Arrays.sort(programs, new ProgramComparator());
int result = 0;
// 从左往右依次遍历所有的会议
for (Program program : programs) {
if (timePoint <= program.start) {
result++;
timePoint = program.end;
}
}
return result;
}
}
题目二:最小字典序
给定一个字符串数组S
,返回一个由S
中所有字符串拼接而成的字符串,要求拼接后的字符串字典序最小。
解题思路:
-
贪心策略的选择,主要就是决定数组中那个字符串放在第一位、第二位、…、第N位,从而使得整个数组拼接起来是字典序最小的,其实就是一个字符串排序问题,排序策略就是贪心策略。
-
排序策略其实也很好想,比如字典序小的排前面(显然不行,可以举出反例:
b
和bba
) -
本题的排序策略应该是:
(a+b)>(b+a)?a放前:b放前
,主要利用了拼接的传递性:由a+b>b+a
和b+c>c+b
推出a+c>c+a
,具体证明比较麻烦,但这就是正确答案(管你看没看懂,写就完了🤯)
public class LowestLexicography {
/**
* 比较策略
*/
public static class MyComparator implements Comparator<String> {
@Override
public int compare(String a, String b) {
return (a + b).compareTo(b + a);
}
}
public static String lowestString(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}
Arrays.sort(strs, new MyComparator());
String res = "";
for (String str : strs) {
res += str;
}
return res;
}
}
对数器验证:
/**
* 枚举方法求最小字典序
*/
public static String enumString(String[] strs) {
ArrayList<String> list = new ArrayList<>(Arrays.asList(strs));
return process(list);
}
public static String process(ArrayList<String> list) {
if (list.size() == 0) {
return "";
}
String min = "~";
for (int i = 0; i < list.size(); i++) {
String temp = list.remove(i);
String s = temp + process(list);
min = min.compareTo(s) < 0 ? min : s;
list.add(i, temp);
}
return min;
}
// 对数器测试
public static void main(String[] args) {
int testTime = 1000;
int maxSize = 10;
int maxValue = 10;
boolean successed = true;
for (int i = 0; i < testTime; i++) {
String[] arr1 = generateRandomArray(maxSize, maxValue);
String[] arr2 = Arrays.copyOf(arr1, arr1.length);
String s1 = enumString(arr1);
String s2 = lowestString(arr2);
if (!s1.equals(s2)) {
System.out.println(s1);
System.out.println(s2);
successed = false;
break;
}
}
System.out.println(successed?"nice!":"Fucking fucked!");
// System.out.println(enumString(new String[]{"b", "bba"}));
}
/**
* 生成随机数组
*/
public static String[] generateRandomArray(int maxSize, int maxLength) {
// 长度随机
String[] arr = new String[(int) (Math.random() * (maxSize + 1))];
for (int i = 0; i < arr.length; i++) {
// 值随机
arr[i] = "";
for (int j = 0; j < (int) (maxLength * Math.random()) + 1; j++) {
arr[i] += (char) (('z' - 'a' + 1) * Math.random() + 'a');
}
}
return arr;
}