数据结构与算法:前缀树和贪心算法

前缀树

一般应用场景如下:两个字符串类型的数组arr1和arr2。求问arr2中有哪些字符是arr1中出现的,有哪些是作为arr1中某个字符串前缀出现的,有哪些字符是出现次数最大的前缀。
这种不直接用哈希表是因为哈希表没办法做到统计以xx为前缀的单词共出现了几次。

板子

class Trie {
    class TrieNode{
        public int pass;//存储的是这个节点被经过几次。譬如nexts[0].pass=1就说明接下来的a节点被经过1次。
        public int end;//存储的是以这个节点为结尾的有几个字符串。
        public TrieNode[] nexts;//存储这个节点的相邻节点

        public TrieNode() {
        pass = 0;
        end = 0;
        nexts = new TrieNode[26];//因为一般题目都是小写字母的字符串,所以只用开26个
        }
    }
    
    public 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.nexts[index]==null) node.nexts[index]=new TrieNode();
            node = node.nexts[index];
            node.pass++; 
        }
        node.end++;
    }
    
    public boolean search(String word) {
        if(word==null) return true;
        char[] chs = word.toCharArray();
        int index = 0;
        TrieNode node = root;
        for(int i=0;i<chs.length;i++){
            index = chs[i]-'a';
            if(node.nexts[index]==null) return false;
            node = node.nexts[index];
        }
        if(node.end==0) return false;
        return true;
    }
    
    public boolean startsWith(String prefix) {//返回是否有以prefix为前缀的字符串
        if(prefix==null) return true;
        char[] chs = prefix.toCharArray();
        int index = 0;
        TrieNode node = root;
        for(int i=0;i<chs.length;i++){
            index = chs[i]-'a';
            if(node.nexts[index]==null) return false;
            node = node.nexts[index];
        }
        return true;
    }
}
class Trie {
    class TrieNode{
        public int pass;
        public int end;
        public TrieNode[] nexts;

        public TrieNode() {
        pass = 0;
        end = 0;
        nexts = new TrieNode[26];
        }
    }
    
    public 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.nexts[index]==null) node.nexts[index]=new TrieNode();
            node = node.nexts[index];
            node.pass++; 
        }
        node.end++;
    }
    
    public boolean search(String word) {
        if(word==null) return true;
        char[] chs = word.toCharArray();
        int index = 0;
        TrieNode node = root;
        for(int i=0;i<chs.length;i++){
            index = chs[i]-'a';
            if(node.nexts[index]==null) return false;
            node = node.nexts[index];
        }
        if(node.end==0) return false;
        return true;
    }

    public int countWordsEqualTo(String word){//查询指定字符串出现了几次
        if(word==null){
            if(root.end==1) return 1;
            else return 0;
        }
        char[] chs = word.toCharArray();
        int index = 0;
        TrieNode node = root;
        for(int i=0;i<chs.length;i++){
            index = chs[i]-'a';
            if(node.nexts[index]==null) return 0;//如果在查询的路上就发现没路了,那么肯定返回0
            node = node.nexts[index];
        }
        return node.end;
    }
    
    public int countWordsStartingWith(String prefix){//查询多少字符串以prefix为前缀
        if(prefix==null) return root.pass;//注意如果前缀为空,那么返回的是字典树中一共存储了多少个字符串
        char[] chs = prefix.toCharArray();
        int index = 0;
        TrieNode node = root;
        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;
    }
    public void erase(String word){//删除
        if(!search(word)) return;//注意一定要先查询字符串是否存在再删除
        char[] chs = word.toCharArray();
        int index = 0;
        TrieNode node = root;
        node.pass--;
        for(int i=0;i<chs.length;i++){
            index = chs[i]-'a';
            if(--node.nexts[index].pass==0) {//说明删到最后一个节点了
                node.nexts[index]=null;
                return;
            }
            node = node.nexts[index];
        }
        node.end--;
    }
}

相关力扣题目

LeetCode 208. 实现 Trie (前缀树)
板子一可解决,用时31ms,击败94.33%使用 Java 的用户。

LeetCode 1804. 实现 Trie (前缀树) II
板子二可解决,用时72ms,击败68.85%使用 Java 的用户

贪心算法

贪心策略在实现时,经常使用到的技巧:
根据某标准建立一个比较器来排序
根据某标准建立一个比较器来组成堆

举例题目:会议室安排

一些项目要占用一个会议室宣讲,会议室不能同时容纳两个项目的宣讲。 给你每一个项目开始的时间和结束的时间(给你一个数组,里面是一个个具体的项目)还有这个会议室开放的最早时间,你来安排宣讲的日程,要求会议室进行的宣讲的场次最多。 返回这个最多的宣讲场次。

我们发现,以会议开始的时间越早越先被安排是不对的,可以轻易举出反例如下,因为这样的话我们只会安排会议a。
在这里插入图片描述

同时,如果以会议持续的时间越短,越先被安排,也是不对的。如下我们只会安排会议b。
在这里插入图片描述

正确的应该是以会议结束的时间越早,越先安排,代码如下:

public 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 p1, Program p2){
            return p1.end-p2.end;
        }
    }
    public boolean ManageMeetings(Program[] programs, int timePoint) {
        Arrays.sort(programs, new ProgramComparator());
        int result = 0;
        for(int i=0;i<programs.length;i++){
            if(programs[i].start>=timePoint){
                timePoint=programs[i].end;
                result++;
            }
        }
        return result;
    }

贪心算法正确性的实战验证(对数器)

1.实现一个不依靠贪心策略的解法X,可以用最暴力的尝试
2.脑补出贪心策略A、贪心策略B、贪心策略C.,.
3.用解法X和对数器,去验证每一个贪心策略

举例:字符串重排后字典序最小(用来说明理论上来验证贪心的正确性是多么复杂的)

给定一组字符串数组,重新排列每个字符串的顺序,使之拼接后组成一个字典序最小的字符串。

如果字符串a和字符串b拼接,a在前的话我们记作a#b,b在前的话我们记作b#a。
这道题的正确贪心策略是,如果a#b的字典序小于b#a的字典序,那么我们优先安排a。
那么如何证明这个策略是有效的呢?首先我们要证明这个策略具有传递性。

什么是传递性,就是譬如一般的数字比较,如果a<b,b<c,那么我们就能得出a<c。
有什么策略是不具有传递性的吗?有的,譬如剪刀石头布就不具有传递性。

也就是说,我们接下来要证明如下:(黄色的小于号代表是字典序小于的意思)
if a#bb#a
and b#cc#b
then a#cc#a
证明具体过程如下:我们记字符串a的长度为stra,字符串b的长度为strb,字符串c的长度为strc
字典序就是将字符串转化为十六进制后,比较数字的大小。那么a#b的字典序就是a16^strb+b(从这里开始,之后的a都代表着原来的字符串转化为十六进制后的数,其他字母同理)
在这我们再次记16^strb为m(b),那么a#b的字典序就是a
m(b)+b,即

a#b=a*m(b)+b
b#a=b*m(a)+a

b#c=b*m(c)+c
c#b=c*m(b)+b

a#c=a*m(c)+c
c#a=c*m(a)+a
我们要证明的再次转变成了(以下的小于号代表的就是数学上的小于号)
if 			a*m(b)+b<b*m(a)+a,记为式子A
and 		b*m(c)+c<c*m(b)+b,记为式子B
then 	a*m(c)+c<c*m(a)+a,记为式子C

我们将式子A-b再*c,得到a*m(b)*c<(b*m(a)+a-b)*c
右边部分展开来,即a*m(b)*c<b*m(a)*c+a*c-b*c
此时左边部分,记作X

我们将式子B-c再*a,得到b*m(c)*a<(c*m(b)+b-c)*a
右边部分展开来,即b*m(c)*a<c*m(b)*a+b*a-c*a
再将右边的最后两项移到左边来,即b*m(c)*a+c*a-b*a<c*m(b)*a
此时右边部分,记作Y

我们发现X=Y。那么就可以推出b*m(c)*a+c*a-b*a<X/Y<b*m(a)*c+a*c-b*c
也就是b*m(c)*a+c*a-b*a<b*m(a)*c+a*c-b*c
把相同项c*a去掉,得到b*m(c)*a-b*a<b*m(a)*c-b*c
再把相同因子b去掉,得到m(c)*a-a<m(a)*c-c
再把左右的常数项调换位置,得到m(c)*a+c<m(a)*c+a
我们发现,这就是式子C,得证

这只是第一步,证明传递性,我们还有之后证明,两两交换位置后,策略没有更优。三三交换位置后,策略没有更优,四四交换位置后,策略没有更优……
所以不建议理论上去证明贪心的正确性,直接用对数器在实践上证明即可。

举例题目:

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鸡鸭扣

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值