作者: KSkun

Trie树原理与实现

Trie树原理与实现

注:本文部分图片来自互联网,其相关权利归原作者所有,感谢原作者的分享。

概述

Trie树是一种具有字符串插入和查找功能的数据结构,又称前缀树。这里介绍它的原理和实现。

原理

结构

180206a 1 - Trie树原理与实现
如上图,这就是一棵Trie树。这个Trie树中包含的单词有abcabdabcdbbcdefghij。可以看到,Trie的字母并不存在结点上,而是存在边上。每个结点的儿子表示以这个结点对应的前缀的字符串的延续。结点的标记代表字符串的终止。这就是一棵Trie树的结构。

查找

了解了它的结构特征后,我们尝试查找一个给定字符串。这一步骤的核心操作就是下跳。从根节点开始,逐字符下跳,直到没有对应的字符或者终止节点无标记,代表查找失败。而如果一直有对应的字符且终止节点有标记代表查找成功。如果能跳出一段,代表当前Trie树中有字符串与查找串有公共前缀。
各位可以在上图中自己尝试一下。

插入

同样是下跳,区别是在找不到字符时候手动建新结点,再给终止结点打标记。各位可以在上图手动尝试插入。

示例

180206a 1a - Trie树原理与实现
查找abd成功。
180206a 1b - Trie树原理与实现
查找abe失败。
180206a 1c - Trie树原理与实现
查找bc失败。
180206a 1d - Trie树原理与实现
插入brqy

实现

准备工作

用一个结构体Node来存储结点信息。

struct Node {
    bool wrd;
    int ch[26];
}

其中wrd表示结点是否是字符串的结尾,ch表示不同字符边对应的儿子编号。
创建Node数组表示整个Trie树,并以Node[1]作为树根。用siz变量统计目前结点数。

查找

inline bool search(char* str) {
    int t = 1;
    for(int i = 0; i < strlen(str); i++) {
        if(!trie[t].ch[str[i] - 'a']) {
            return false;
        } else {
            t = trie[t].ch[str[i] - 'a'];
        }
    }
    if(trie[t].wrd) {
        return true;
    } else {
        return false;
    }
}

模拟下跳过程,检查是否有结点、是否有标记。

插入

inline void insert(char* str) {
    int t = 1;
    for(int i = 0; i < strlen(str); i++) {
        if(!trie[t].ch[str[i] - 'a']) {
            t = trie[t].ch[str[i] - 'a'] = ++siz;
        } else {
            t = trie[t].ch[str[i] - 'a'];
        }
    }
    trie[t].wrd = true;
}

动态开节点,加标记。

一道例题:于是他错误的点名开始了

题意

插入字符串,查找字符串,判断字符串找没找过。

题解

Trie可以胜任前两个操作。至于判断是否找过,在结束结点上记录访问过没有即可。

代码

// Code by KSkun, 2018/2
#include <cstdio>
#include <cstring>

struct Node {
    bool wrd, vis;
    int ch[26];
} trie[500005];
int siz = 1;

inline void insert(char* str) {
    int t = 1;
    for(int i = 0; i < strlen(str); i++) {
        if(!trie[t].ch[str[i] - 'a']) {
            t = trie[t].ch[str[i] - 'a'] = ++siz;
        } else {
            t = trie[t].ch[str[i] - 'a'];
        }
    }
    trie[t].wrd = true;
}

inline int search(char* str) {
    int t = 1;
    for(int i = 0; i < strlen(str); i++) {
        if(!trie[t].ch[str[i] - 'a']) {
            return 0;
        } else {
            t = trie[t].ch[str[i] - 'a'];
        }
    }
    if(trie[t].wrd) {
        if(trie[t].vis) return -1;
        trie[t].vis = true;
        return 1;
    } else {
        return 0;
    }
}

int n, m;
char str[55];

int main() {
    scanf("%d", &n);
    for(int i = 0; i < n; i++) {
        scanf("%s", str);
        insert(str);
    }
    scanf("%d", &m);
    for(int i = 0; i < m; i++) {
        scanf("%s", str);
        int res = search(str);
        if(res == 1) printf("OK\n");
        else if(res == -1) printf("REPEAT\n");
        else printf("WRONG\n");
    }
    return 0;
}
[ZJOI2004]午餐 题解

[ZJOI2004]午餐 题解

题目地址:洛谷:【P2577】[ZJOI2005]午餐 – 洛谷、BZOJ:Problem 1899. — [Zjoi2004]Lunch 午餐

题目描述

上午的训练结束了,THU ACM小组集体去吃午餐,他们一行N人来到了著名的十食堂。这里有两个打饭的窗口,每个窗口同一时刻只能给一个人打饭。由于每个人的口味(以及胃口)不同,所以他们要吃的菜各有不同,打饭所要花费的时间是因人而异的。另外每个人吃饭的速度也不尽相同,所以吃饭花费的时间也是可能有所不同的。
THU ACM小组的吃饭计划是这样的:先把所有的人分成两队,并安排好每队中各人的排列顺序,然后一号队伍到一号窗口去排队打饭,二号队伍到二号窗口去排队打饭。每个人打完饭后立刻开始吃,所有人都吃完饭后立刻集合去六教地下室进行下午的训练。
现在给定了每个人的打饭时间和吃饭时间,要求安排一种最佳的分队和排队方案使得所有人都吃完饭的时间尽量早。
假设THU ACM小组在时刻0到达十食堂,而且食堂里面没有其他吃饭的同学(只有打饭的师傅)。每个人必须而且只能被分在一个队伍里。两个窗口是并行操作互不影响的,而且每个人打饭的时间是和窗口无关的,打完饭之后立刻就开始吃饭,中间没有延迟。
现在给定N个人各自的打饭时间和吃饭时间,要求输出最佳方案下所有人吃完饭的时刻。

输入输出格式

输入格式:
第一行一个整数N,代表总共有N个人。
以下N行,每行两个整数 Ai,Bi。依次代表第i个人的打饭时间和吃饭时间。

输出格式:
一个整数T,代表所有人吃完饭的最早时刻。

输入输出样例

输入样例#1:

5
2 2
7 7
1 3
6 4
8 5

输出样例#1:

17

说明

所有输入数据均为不超过200的正整数。

题解

本题是一个贪心加DP的题目。首先我们可以将题目分为两部分:排序和分配。

排序:贪心

贪心的依据是吃饭慢的先打饭。这个在我们的直觉中就是正确的,而证明也很容易。
假设队列中有两个人i和j,他们有确定关系B_i < B_j。如果按照i j这样的顺序排列,他们两个人产生的答案分别是A_i + B_iA_i + A_j + B_j。这里显然最终答案是后者。如果按照j i的顺序排列,则变为A_j + B_jA_j + A_i + B_i。这两个答案都不会比第一种情况的答案大。因此我们确定j i排列比i j排列更优。

分配:DP

设计状态dp[i][j]表示现在排到第i个人,第一队打饭要用j时间的吃完饭时刻。现在分情况讨论第i个人是加入第一队还是加入第二队。
加入第一队:
dp[i][j + A_i] = \max \{dp[i - 1][j], j + A_i + B_i\}
加入第二队:
dp[i][j] = \max \{dp[i - 1][j], S_i - j + B_i\}
其中S表示从第一个人到第i个人的打饭时间前缀和。
我们发现第一维数据为n,第二维是n^2,空间复杂度O(n^3)。这是可以接受的,但是仍然可以优化。
总时间复杂度:O(n^3)

优化空间:滚动数组和降维

与其他可以用这种优化空间的方法的题目相似,这里不再赘述。

代码

// Code by KSkun, 2018/2
#include <cstdio>
#include <cstring>
#include <algorithm>

struct Node {
    int a, b;
};

int n, dp[40005], sum = 0, ans = 1e9;
Node p[205];

inline bool cmp(Node a, Node b) {
    return a.b > b.b;
}

int main() {
    scanf("%d", &n);
    for(int i = 1; i <= n; i++) {
        scanf("%d%d", &p[i].a, &p[i].b);
    }
    std::sort(p + 1, p + n + 1, cmp);
    memset(dp, 0x3f, sizeof dp);
    dp[0] = 0;
    for(int i = 1; i <= n; i++) {
        for(int j = sum; j >= 0; j--) {
            dp[j + p[i].a] = std::min(dp[j + p[i].a], 
                std::max(dp[j], j + p[i].a + p[i].b)); // 加入第一队 
            dp[j] = std::max(dp[j], sum + p[i].a - j + p[i].b); // 加入第二队 
        }
        sum += p[i].a;
    }
    for(int i = 1; i <= sum; i++) {
        ans = std::min(ans, dp[i]);
    }
    printf("%d", ans);
    return 0;
}
单调队列原理及其应用

单调队列原理及其应用

概述

单调队列是一种广泛应用的数据结构,它能够动态地维护定长序列中的最值,可以应用于求最值与优化DP转移等方面。本文章将讲述单调队列的原理、实现及应用。
除本文提到的例子以外,更多示例可以参见DP的花式优化方法 | KSkun’s Blog

原理

单调队列实质上是一类双端队列(deque),在加入一个元素时,我们通过一定操作维护队列的单调性。该队列特征的操作便是“后移一位”。顾名思义,后移一位就是指队列维护的区间往后移一位。这个操作中要做的事情是弹出队首与加入队尾。

加入队尾

为了保证这个队列中元素的单调性,对于加入的元素,我们可能要删除一些队尾的元素。以不下降的单调队列为例(下同),插入时要弹出所有比要插入元素大的队尾元素,直到队尾元素不再比其大再插入。此时插入元素时要记下插入元素在原序列中的位置,这个信息将会对弹出队首时发挥作用。

弹出队首

对于插入的元素,我们检查它与队首元素的位置之差,当这个差大于队列定长时说明队首元素已在区间之外,应当弹出。

例子

下面来看一组例子,以加深理解。我们维护一个序列中,定长为3的递增区间。这个序列给定为1 3 2 6 4 9 0 11
先向队列中插入1。此时队列为1(1)
由于长度未达到3,直接再插入3。此时队列为1(1) 3(2)
插入2时,发现队尾存在比2大的3,弹出3。此时队列为1(1) 2(3)。队列长已满足3。
再插入6,发现序列头元素已经在区间外,弹出1。此时队列为2(3) 6(4)
接下来的步骤不再描述。

实现

假设我们在实现一种维护长为k的递增区间,且序列a总长为n。
注:以下代码未测试,仅供参考,如有错误欢迎在评论指正。

STL deque

que为双端队列。队列中元素为结构体,存有值(v)和位置(pos)。

for(int i = 1; i <= n; i++) {
    while(que.back().v >= a[i]) que.pop_back();
    que.push_back(Node(a[i], i));
    if(que.front().pos == i - n) que.pop_front();
}

手写队列

l、r为队首、队尾指针,pos为位置。

for(int i = 1; i <= n; i++) {
    while(que[r - 1] >= a[i]) r--;
    que[r] = a[i];
    pos[r] = i;
    r++;
    if(pos[l] == i - n) l++;
}

应用

维护定长区间内最值:[HAOI2007]理想的正方形

题目描述

有一个a*b的整数组成的矩阵,现请你从中找出一个n*n的正方形区域,使得该区域所有数中的最大值和最小值的差最小。

输入输出格式

输入格式:
第一行为3个整数,分别表示a,b,n的值
第二行至第a+1行每行为b个非负整数,表示矩阵中相应位置上的数。每行相邻两数之间用一空格分隔。
输出格式:
仅一个整数,为a*b矩阵中所有“n*n正方形区域中的最大整数和最小整数的差值”的最小值。

输入输出样例

输入样例#1:

5 4 2
1 2 5 6
0 17 16 0
16 17 2 1
2 10 2 1
1 2 2 2

输出样例#1:

1

说明

问题规模

  1. 矩阵中的所有数都不超过1,000,000,000
  2. 20%的数据2 \leq a,b \leq 100,n \leq 10
  3. 100%的数据2 \leq a,b \leq 1000,n \leq a,n \leq b,n \leq 100

题解

在本题中,单调队列发挥了与ST表相似的功能,即维护定长区间最值。我们考虑这个正方形是由n行长为n的区间构成的。因此可以对原矩阵中每一行的每一个长为n的区间分别求最大最小值,记录下来,再在记录的数组中对每列长为n的区间分别求最大最小值。其最小差即为答案。这种做法相当于先将行上的区间抽象成一个点,这样一个正方形相当于列上的一个区间。

代码

// Code by KSkun, 2018/1 
#include <cstdio>
#include <algorithm>

struct io {
    char buf[1 << 26], *s;

    io() {
        fread(s = buf, 1, 1 << 26, stdin);
    }

    inline int read() {
        register int res = 0, neg = 1;
        while(*s < '0' || *s > '9') if(*(s++) == '-') neg = -1;
        while(*s >= '0' && *s <= '9') res = res * 10 + *s++ - '0';
        return res * neg;
    }
} ip;

#define read ip.read

int a, b, n, mat[1005][1005], pos[1005], mx[1005][1005], mn[1005][1005], 
    que[1005], mmx[1005], mmn[1005], l, r, ans = 1e9;

int main() {
    a = read();
    b = read();
    n = read();
    for(int i = 1; i <= a; i++) {
        for(int j = 1; j <= b; j++) {
            mat[i][j] = read();
        }
    }
    for(int i = 1; i <= a; i++) {
        l = r = 1;
        for(int j = 1; j <= b; j++) {
            while(l < r && que[r - 1] <= mat[i][j]) r--;
            que[r] = mat[i][j];
            pos[r] = j;
            r++;
            if(pos[l] == j - n) l++;
            if(j >= n) mx[i][j] = que[l];
        }
        l = r = 1;
        for(int j = 1; j <= b; j++) {
            while(l < r && que[r - 1] >= mat[i][j]) r--;
            que[r] = mat[i][j];
            pos[r] = j;
            r++;
            if(pos[l] == j - n) l++;
            if(j >= n) mn[i][j] = que[l];
        }
    }
    for(int i = n; i <= b; i++) {
        l = r = 1;
        for(int j = 1; j <= a; j++) {
            while(l < r && que[r - 1] <= mx[j][i]) r--;
            que[r] = mx[j][i];
            pos[r] = j;
            r++;
            if(pos[l] == j - n) l++;
            if(j >= n) mmx[j] = que[l];
        }
        l = r = 1;
        for(int j = 1; j <= a; j++) {
            while(l < r && que[r - 1] >= mn[j][i]) r--;
            que[r] = mn[j][i];
            pos[r] = j;
            r++;
            if(pos[l] == j - n) l++;
            if(j >= n) mmn[j] = que[l];
        }
        for(int j = n; j <= a; j++) {
            ans = std::min(ans, mmx[j] - mmn[j]);
        }
    }
    printf("%d", ans);
    return 0;
}

优化DP转移的复杂度:POJ2373 Dividing the Path

题目大意

有一段[0, L]的大草场,现在在其中某些不要求为整点的位置安装洒水器使得每个点都恰好被1个洒水器的洒水范围覆盖。洒水器的范围可以调节,范围半径为[A, B]。草场中有一些区间必须整个被恰好1个洒水器所覆盖。
数据范围:1 \leq L \leq 10^6, 1 \leq A \leq B \leq 1000, 1 \leq N \leq 1000

题解

考虑DP解决这个问题。
用dp[i]表示[0, i]区间被完全覆盖所需的最少洒水器数量。可以发现,如果i是一个奇数,那么不存在一种覆盖的方案,因为洒水器覆盖的区间长度[2A, 2B],必须为偶数。转移方程如下。
dp[i] = \min_{j=i-2B}^{i-2A} dp[j] + 1
上述方程中,除要求j的取值外还要求j处存在合法方案以及j不是题目描述的特殊区间。
关于特殊区间的处理,直接令区间内的位置dp值为INF。这样它是不会被转移的,也就是说,不存在一种方案会使得这个区间内的位置成为两个洒水器范围的分界点。
然而我们发现暴力遍历转移的复杂度是O(n \cdot (2B - 2A))的,对于数据范围还是不够,考虑用单调队列优化中间这个找最小值的过程。
找最小值就是一个长度为2B-2A的区间在序列上滑动的过程,自然可以用单调队列维护这个序列中的最小值,至此,我们可以将算法优化至O(n)

代码

吐槽:写的时候符号写反了WA了一次才过233

// Code by KSkun, 2018/1
#include <cstdio>
#include <cstring>
#include <deque>
#include <algorithm> 

const int INF = 0x3f3f3f3f;

struct Node {
    int v, pos;
    Node(int v, int pos): v(v), pos(pos) {} 
};

std::deque<Node> que;
int n, l, a, b, xt, yt, dp[1000005], tag[1000005]; 

int main() {
    memset(dp, 0x3f, sizeof dp);
    scanf("%d%d%d%d", &n, &l, &a, &b);
    if(l & 1) { // l为奇数时无法被完全覆盖 
        printf("-1");
        return 0;
    }
    for(int i = 0; i < n; i++) {
        scanf("%d%d", &xt, &yt);
        tag[xt + 1] += 1;
        tag[yt] -= 1; 
    }
    for(int i = 1; i <= l; i++) {
        tag[i] += tag[i - 1];
    }
    dp[0] = 0;
    for(int i = 2; i <= l; i += 2) {
        if(i - 2 * a >= 0 && !tag[i - 2 * a] && !((i - 2 * a) & 1)) {
            while(!que.empty() && que.back().v >= dp[i - 2 * a]) {
                que.pop_back();
            }
            que.push_back(Node(dp[i - 2 * a], i - 2 * a));
        }
        while(!que.empty() && que.front().pos < i - 2 * b) que.pop_front();
        if(!tag[i] && !que.empty()) {
            dp[i] = std::min(dp[i], que.front().v + 1);
        }
    }
    if(dp[l] >= INF) {
        printf("-1");
    } else {
        printf("%d", dp[l]);
    }
    return 0;
}