前缀树
介绍
何为前缀树?如何构建前缀树?以如下字符串为例:
["abc", "bck", "abd", "ace"]
首先构建一个空节点(根节点),然后开始遍历字符串数组。对于每一个字符串,遍历其中的字符,对每一个字符 c ,判断当前节点有无 c 的路径,无则添加,如下图:
目前的节点上还未存储值,实际上节点上可以存储有多少个字符串经过它,以它结尾的字符串有多少个,如下图(其中p代表经过该节点的字符串数量,e代表以该节点为结尾的字符串数量):
利用这颗前缀树,我们可以很方便的知道以 “ab” 为前缀的字符串数量——即红圈节点的p值2。
代码表示
// 前缀树的节点
public static class TrieNode {
// 通过该节点的字符串数量
public int pass;
// 以该节点为结尾的字符串数量
public int end;
// 可以通往的其他节点,仅考虑小写字母可以用数组,若字符较为复杂可以用哈希表:HashMap<Char, TrieNode> 来表示,若需求路径有序,还可以用有序表TreeMap<Char, Node>表示
public TrieNode[] nexts;
public TrieNode() {
pass = 0;
end = 0;
// 如果仅考虑小写字母,初始化为26
nexts = 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) {
// 遍历字符串,寻找路径,这里其实是把a-z转化为了下标,如果前面用哈希表表示,这里的处理方式改为哈希表相应的处理即可
index = chs[i] - 'a';
if (node.nexts[index] == null) {
node.nexts[index] = new TrieNode();
}
node = node.nexts[index];
node.pass++;
}
node.end++;
}
// 从前缀树中删除字符串
public void delete(String word) {
// 先查是否添加过该字符,添加过才可以删除
if (search(word) != 0) {
char[] chs = word.toCharArray();
TrieNode node = root;
node.pass--;
int index = 0;
for (int i = 0; i < chs.length; ++i) {
index = chs[index] - 'a';
if (--node.nexts[index].pass == 0) {
// 删除当前字符后之后节点的p值为0(没有字符串经过该节点),之后所有节点均删除
node.nexts[index] = null;
// 如果是没有垃圾回收机制的语言,需要记录所有待删除节点,手动释放内存
return;
}
node = node.nexts[index];
}
node.end--;
}
}
// 搜索前缀树中对应字符串加入过几次
public int search(String word) {
if (word == null) {
return 0;
}
char[] chs = wrod.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; ++i) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.end;
}
// 所有加入的字符串中,有几个是以 pre 这个字符串作为前缀的
public int prefixNumber(String pre) {
if (pre == null) {
return 0;
}
char[] chs = pre.toCharArray();
TrieNode node = root;
int index = 0;
for (int i = 0; i < chs.length; ++i) {
index = chs[i] - 'a';
if (node.nexts[index] == null) {
return 0;
}
node = node.nexts[index];
}
return node.pass;
}
}
贪心算法
介绍
在某种标准下,优先考虑最满足标准的样本,最后考虑最不满足标准的样本,最终得到一个答案的算法,叫贪心算法。注意,贪心算法得到的是在某种意义上的局部最优解,如果要靠贪心算法得到整体最优解,需要证明这个局部最优与整体最优等价。
例题
分配会议室
题目说明
现在有一些项目需要使用同一个会议室进行产品宣讲,每个项目的宣讲都有一个开始和结束时间,会议室从早上8点开放到晚上6点(所有项目的宣讲起止时间都在这个范围内),请你在一天之内尽可能安排多的项目宣讲。
贪心策略:优先选择结束时间早的项目。
代码
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;
}
}
public static int bestArrange(Program[] programs) {
Arrays.sort(programs, new ProgramComparator());
int timePoint = 8;
int result = 0;
for (int i = 0; i < programs.length; ++i) {
// 当前时间点是否早于该项目的宣讲开始时间
if (timePoint <= programs[i].start) {
result++;
timePoint = programs[i].end;
}
}
return result;
}
最小字典序的字符串
题目说明
有一个字符串数组,你需要按照一定的顺序将里面的所有字符串拼接成一个大字符串,使得这个大字符串的字典序尽可能小。
贪心策略:假设有两字符串a、b,若a+b的字典序不大于b+a,则a优先。
代码
public static class MyStringComparator 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 MyStringComparator());
String res = "";
for (int i = 0; i < strs.length; ++i) {
res += strs[i];
}
return res;
}
切金条
题目说明
一根金条切成两根,需要花费和长度数值一样的铜板。比如长度为20的金条,不管切成长度多少的两半,都需要花费20个铜板。一群人想要分配一根金条,怎么切最省铜板?
例如,给定数组[10,20,30]
,代表金条长度为60,第一个人需要长度为10的金条,第二个人需要长度为20的,第三个人30,请返回切割的最小代价。
思路
经典的 哈夫曼编码 问题。我们首先把需要的所有金条长度放入一个小根堆,每次弹出两个长度a和b,将 a+b 加到代价中,再将 a+b 压回小根堆,重复操作直至小根堆长度小于2,即是最小代价。(每次从最短的两根开始回溯)
代码
public static int lessMoney(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;
}
项目的最大收益
题目说明
输入:
正整数数组costs、正整数数组profits、正整数k、正整数m。
含义:
costs[i]代表 i 号项目的花费、profits[i]代表 i 号项目扣除花费后还能获得的钱(净利润)、k代表你最多能进行的总项目数、m代表你的初始资金。
说明:
你每做完一个项目,马上获得收益。
输出:
你能获得的最大钱数。
思路
利用一个小根堆一个大根堆,首先把所有项目按花费排序全部放到小根堆中,然后每次做项目之前根据当前的资金,把所有花费不大于当前资金的项目弹出小根堆,按利润排序放入大根堆中,选大根堆堆顶的项目执行,重复操作即可得到最大收益。
代码
public static class Node {
public int p;
public 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) {
return o1.c - o2.c;
}
}
public static class MaxProfitComparator implements Comparator<Node> {
@Override
public int compare(Node o1, Node o2) {
// 注意是降序排序
return o2.p - o1.p;
}
}
public static int findMaximizedCapital(int k, int m, int[] profits, int[] costs) {
PriorityQueue<Node> minCostQ = new PriorityQueue<>(new MinCostComparator());
PriorityQueue<Node> maxProfitQ = new PriorityQueue<>(new MaxProfitComparator());
for (int i = 0; i < profits.length; ++i) {
minCostQ.add(new Node(profits[i], costs[i]));
}
for (int i = 0; i < k; ++i) {
while (!minCostQ.isEmpty() && minCostQ.peek().c <= m) {
maxProfitQ.add(minCostQ.poll());
}
if (maxProfitQ.isEmpty()) {
// 没有能做的项目了
return m;
}
m += maxProfitQ.poll().p;
}
return m;
}
解题套路
- 实现一个不依靠贪心的解法X,可以用暴力方式;
- 构思可能的贪心策略A、B、C…
- 用解法X验证每个贪心策略,寻找可能正确的那个;
- 实现贪心策略时的常用技巧:自定比较器的排序、小根堆。