【算法学习笔记】前缀树、贪心算法

文章介绍了前缀树的概念、构建方法以及Java代码实现,包括插入、删除和搜索操作。同时,展示了贪心算法的应用,如优先选择结束时间早的会议室分配问题,最小字典序字符串生成,以及金条切割的最小代价计算。每个问题都提供了详细的解题思路和代码示例。
摘要由CSDN通过智能技术生成

前缀树

介绍

何为前缀树?如何构建前缀树?以如下字符串为例:
["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;
}

解题套路

  1. 实现一个不依靠贪心的解法X,可以用暴力方式;
  2. 构思可能的贪心策略A、B、C…
  3. 用解法X验证每个贪心策略,寻找可能正确的那个;
  4. 实现贪心策略时的常用技巧:自定比较器的排序、小根堆。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值