Trie树原理与实现
注:本文部分图片来自互联网,其相关权利归原作者所有,感谢原作者的分享。 概述 Trie树是 …
May all the beauty be blessed.
题目地址:洛谷:【P1070】道路游戏 – 洛谷
小新正在玩一个简单的电脑游戏。
游戏中有一条环形马路,马路上有 n 个机器人工厂,两个相邻机器人工厂之间由一小段马路连接。小新以某个机器人工厂为起点,按顺时针顺序依次将这 n 个机器人工厂编号为1~n,因为马路是环形的,所以第 n 个机器人工厂和第 1 个机器人工厂是由一段马路连接在一起的。小新将连接机器人工厂的这 n 段马路也编号为 1~n,并规定第 i 段马路连接第 i 个机器人工厂和第 i+1 个机器人工厂(1≤i≤n-1),第 n 段马路连接第 n 个机器人工厂和第 1个机器人工厂。
游戏过程中,每个单位时间内,每段马路上都会出现一些金币,金币的数量会随着时间发生变化,即不同单位时间内同一段马路上出现的金币数量可能是不同的。小新需要机器人的帮助才能收集到马路上的金币。所需的机器人必须在机器人工厂用一些金币来购买,机器人一旦被购买,便会沿着环形马路按顺时针方向一直行走,在每个单位时间内行走一次,即从当前所在的机器人工厂到达相邻的下一个机器人工厂,并将经过的马路上的所有金币收集给小新,例如,小新在 i(1≤i≤n)号机器人工厂购买了一个机器人,这个机器人会从 i 号机器人工厂开始,顺时针在马路上行走,第一次行走会经过 i 号马路,到达 i+1 号机器人工厂(如果 i=n,机器人会到达第 1 个机器人工厂),并将 i 号马路上的所有金币收集给小新。 游戏中,环形马路上不能同时存在 2 个或者 2 个以上的机器人,并且每个机器人最多能够在环形马路上行走 p 次。小新购买机器人的同时,需要给这个机器人设定行走次数,行走次数可以为 1~p 之间的任意整数。当马路上的机器人行走完规定的次数之后会自动消失,小新必须立刻在任意一个机器人工厂中购买一个新的机器人,并给新的机器人设定新的行走次数。
以下是游戏的一些补充说明:
现在已知每段马路上每个单位时间内出现的金币数量和在每个机器人工厂购买机器人需要的花费,请你告诉小新,经过 m 个单位时间后,扣除购买机器人的花费,小新最多能收集到多少金币。
输入格式:
第一行 3 个正整数,n,m,p,意义如题目所述。
接下来的 n 行,每行有 m 个正整数,每两个整数之间用一个空格隔开,其中第 i 行描述了 i 号马路上每个单位时间内出现的金币数量(1≤金币数量≤100),即第 i 行的第 j(1≤j≤m)个数表示第 j 个单位时间内 i 号马路上出现的金币数量。
最后一行,有 n 个整数,每两个整数之间用一个空格隔开,其中第 i 个数表示在 i 号机器人工厂购买机器人需要花费的金币数量(1≤金币数量≤100)。
输出格式:
共一行,包含 1 个整数,表示在 m 个单位时间内,扣除购买机器人花费的金币之后,小新最多能收集到多少金币。
输入样例#1:
2 3 2 1 2 3 2 3 4 1 2
输出样例#1:
5
【数据范围】
对于 40%的数据,2≤n≤40,1≤m≤40。
对于 90%的数据,2≤n≤200,1≤m≤200。
对于 100%的数据,2≤n≤1000,1≤m≤1000,1≤p≤m。
NOIP 2009 普及组 第四题
这里我采用的是GhastIcon同学的思路。可以在这里P1070 道路游戏 题解找到他的题解。
设计状态dp[i]为到第i时刻获得的最大金币数,转移可以通过枚举这个时刻到的位置和走过的步数来实现,方程如下
dp[i] = \max \{dp[i - t] + g[i][s - 1] - g[i][s - t - 1] - c[s - t]\}
具体解释一下上面的各个变量。t是枚举的步数,s是枚举的当前位置。i-t表示t时间前的时刻,p-t表示t时间前的位置(要注意处理一下让它在1~n范围内),g表示一种斜线的前缀和,c表示在这个位置买机器人的开销。可以知道我们的收益在题目给的那个表上是对角线上计算的,以样例为例,两种斜线前缀和如下图。
由此我们知道这个DP是O(mn^2)的。这种思路对于这道题来说不够优。我们需要优化。
整理一下DP方程。
dp[i] = \max \{dp[i - t] - g[i][s - t - 1] - c[s - t]\} + g[i][s - 1]
我们发现max标记里面的这个值其实可以单调队列处理,因为枚举的t有一个范围1 \leq t \leq p。设一个 h[i][j] = dp[i] - g[i][j - 1] - c[j] ,用h代换方程中的max里面的部分,单调队列维护的就是这个h。对于h的每一个j创建单调队列,在更新i的同时更新单调队列,这样就可以达到降低时间复杂度的目的。转移变为O(n)。
这里的思路去发现依赖关系和最值转移关系。
感觉dp数组都可以拿掉了,因为最后dp出来的值都是要插入单调队列里面的。可以稍稍优化空间常数。
初始值要设成买一个机器人的开销。以及我代码中的deque可以全换成手写双端队列,STL比较慢。
// Code by KSkun, 2018/2
#include <cstdio>
#include <cstring>
#include <deque>
#include <utility>
typedef std::pair<int, int> PI;
std::deque<PI> que[1005];
int n, m, p, sum[1005][1005], cost[1005], dp[1005], v;
inline int minus(int a, int b) { // return a - b
return ((a - b) % n + n) % n;
}
int main() {
scanf("%d%d%d", &n, &m, &p);
for(int i = 0; i < n; i++) {
for(int j = 1; j <= m; j++) {
scanf("%d", &sum[i][j]);
}
}
for(int i = 0; i < n; i++) {
scanf("%d", &cost[i]);
}
// 处理对角线前缀和
for(int i = 2; i <= m; i++) {
for(int j = 0; j < n; j++) {
sum[j][i] += sum[minus(j, 1)][i - 1];
}
}
// 设置初值表示开始一定要买一个机器人
for(int i = 0; i < n; i++) {
que[minus(i, -1)].push_back(std::make_pair(-cost[i], 0));
}
memset(dp, 0xc0, sizeof dp);
for(int i = 1; i <= m; i++) {
for(int j = 0; j < n; j++) {
dp[i] = std::max(dp[i], que[minus(j, i - 1)].front().first + sum[minus(j, 1)][i]);
}
for(int j = 0; j < n; j++) {
while(!que[minus(j, i - 1)].empty() && i - que[minus(j, i - 1)].front().second >= p) {
que[minus(j, i - 1)].pop_front();
}
v = dp[i] - sum[minus(j, 1)][i] - cost[j];
while(!que[minus(j, i - 1)].empty() && que[minus(j, i - 1)].back().first <= v) {
que[minus(j, i - 1)].pop_back();
}
que[minus(j, i - 1)].push_back(std::make_pair(v, i));
}
}
printf("%d", dp[m]);
return 0;
}
题目地址:【P1273】有线电视网 – 洛谷
某收费有线电视网计划转播一场重要的足球比赛。他们的转播网和用户终端构成一棵树状结构,这棵树的根结点位于足球比赛的现场,树叶为各个用户终端,其他中转站为该树的内部节点。
从转播站到转播站以及从转播站到所有用户终端的信号传输费用都是已知的,一场转播的总费用等于传输信号的费用总和。
现在每个用户都准备了一笔费用想观看这场精彩的足球比赛,有线电视网有权决定给哪些用户提供信号而不给哪些用户提供信号。
写一个程序找出一个方案使得有线电视网在不亏本的情况下使观看转播的用户尽可能多。
输入格式:
输入文件的第一行包含两个用空格隔开的整数N和M,其中2 \leq N \leq 3000,1 \leq M \leq N-1,N为整个有线电视网的结点总数,M为用户终端的数量。
第一个转播站即树的根结点编号为1,其他的转播站编号为2到N-M,用户终端编号为N-M+1到N。
接下来的N-M行每行表示—个转播站的数据,第i+1行表示第i个转播站的数据,其格式如下:
K A1 C1 A2 C2 … Ak Ck
K表示该转播站下接K个结点(转播站或用户),每个结点对应一对整数A与C,A表示结点编号,C表示从当前转播站传输信号到结点A的费用。最后一行依次表示所有用户为观看比赛而准备支付的钱数。
输出格式:
输出文件仅一行,包含一个整数,表示上述问题所要求的最大用户数。
输入样例#1:
5 3 2 2 2 5 3 2 3 2 4 3 3 4 2
输出样例#1:
2
样例解释
如图所示,共有五个结点。结点①为根结点,即现场直播站,②为一个中转站,③④⑤为用户端,共M个,编号从N-M+1到N,他们为观看比赛分别准备的钱数为3、4、2,从结点①可以传送信号到结点②,费用为2,也可以传送信号到结点⑤,费用为3(第二行数据所示),从结点②可以传输信号到结点③,费用为2。也可传输信号到结点④,费用为3(第三行数据所示),如果要让所有用户(③④⑤)都能看上比赛,则信号传输的总费用为:2+3+2+3=10,大于用户愿意支付的总费用3+4+2=9,有线电视网就亏本了,而只让③④两个用户看比赛就不亏本了。
显然是一个树形DP,但是每个子树怎么处理也就成了问题。对于每一个终端用户,决策是选或不选,可以看成一个树上的01背包,因此设计状态为dp[u][i]表示节点u给i个用户提供服务的利润,转移方程如下
dp[u][i+k] = max\{dp[u][i+k], dp[u][i] + dp[v][k] - w[u][v]\}
其中,v是u的子节点,k是一个枚举的量,表示从子节点选择多少用户提供服务。依照01背包的顺序枚举即可。最后的答案在dp[1][i]中,取dp值非负的i最大值即可。
// Code by KSkun, 2017/12
#include <cstdio>
#include <cstring>
#include <vector>
#include <algorithm>
struct io {
char buf[1 << 26], *s;
io() {
fread(s = buf, 1, 1 << 26, stdin);
}
inline int read() {
register int res = 0;
while(*s < '0' || *s > '9') s++;
while(*s >= '0' && *s <= '9') res = res * 10 + *s++ - '0';
return res;
}
};
io ip;
#define read ip.read
struct Edge {
int to, w;
Edge(int to, int w): to(to), w(w) {}
};
std::vector<Edge> vec[3005];
inline void addedge(int u, int v, int w) {
vec[u].push_back(Edge(v, w));
vec[v].push_back(Edge(u, w));
}
int n, m, w[3005], kt, at, ct, fa[3005], siz[3005], dp[3005][3005];
inline void dfs(int u) {
dp[u][0] = 0;
if(vec[u].size() == 1) {
siz[u] = 1;
dp[u][1] = w[u];
return;
}
for(int i = 0; i < vec[u].size(); i++) {
int v = vec[u][i].to;
if(v == fa[u]) continue;
fa[v] = u;
dfs(v);
for(int j = siz[u]; j >= 0; j--) {
for(int k = siz[v]; k >= 0; k--) {
dp[u][j + k] = std::max(dp[u][j + k], dp[u][j] + dp[v][k] - vec[u][i].w);
}
}
siz[u] += siz[v];
}
}
int main() {
memset(dp, 0xc0, sizeof dp);
n = read();
m = read();
for(int i = 1; i <= n - m; i++) {
kt = read();
for(int j = 1; j <= kt; j++) {
at = read();
ct = read();
addedge(i, at, ct);
}
}
for(int i = n - m + 1; i <= n; i++) {
w[i] = read();
}
dfs(1);
for(int i = m; i >= 0; i--) {
if(dp[1][i] >= 0) {
printf("%d", i);
return 0;
}
}
return 0;
}