Codeforces Round #670 (Div. 2) 题解
[CF1406A] Subset Mex 提交地 …
May all the beauty be blessed.
题目地址:洛谷:P5659 树上的数 – 洛谷 | 计算机科学教育新生态
给定一个大小为 $n$ 的树,它共有 $n$ 个结点与 $n-1$ 条边,结点从 $1 \sim n$ 编号。初始时每个结点上都有一个 $1 \sim n$ 的数字,且每个 $1 \sim n$ 的数字都只在恰好一个结点上出现。
接下来你需要进行恰好 $n-1$ 次删边操作,每次操作你需要选一条未被删去的边,此时这条边所连接的两个结点上的数字将会交换,然后这条边将被删去。
$n-1$ 次操作过后,所有的边都将被删去。此时,按数字从小到大的顺序,将数字 $1 \sim n$ 所在的结点编号依次排列,就得到一个结点编号的排列 $P_i$ 。现在请你求出,在最优操作方案下能得到的字典序最小的 $P_i$ 。
如上图,蓝圈中的数字 $1 \sim 5$ 一开始分别在结点②、①、③、⑤、④。按照 (1)(4)(3)(2) 的顺序删去所有边,树变为下图。按数字顺序得到的结点编号排列为①③④②⑤,该排列是所有可能的结果中字典序最小的。
一棵 $n$ 个节点的树上每个节点有一个 $1 \sim n$ 中的数字,定义对边 $(u, v)$ 进行删边操作为删边且交换 $u, v$ 节点上的数字,定义 $P_i$ 为数字 $i$ 所在的节点编号,求使得 $P_i$ 字典序最小的删边顺序。仅输出字典序最小的 $P_i$ 。
输入格式:
从文件 tree.in 中读入数据。
本题输入包含多组测试数据。
第一行一个正整数 $T$ ,表示数据组数。
对于每组测试数据:
第一行一个整数 $n$ ,表示树的大小。
第二行 $n$ 个整数,第 $i \ (1 \leq i \leq n)$ 个整数表示数字 $i$ 初始时所在的结点编号。
接下来 $n – 1$ 行每行两个整数 $x, y$ ,表示一条连接 $x$ 号结点与 $y$ 号结点的边。
输出格式:
输出到文件 tree.out 中。
对于每组测试数据,输出一行共 $n$ 个用空格隔开的整数,表示最优操作方案下所能得到的字典序最小的 $P_i$ 。
输入样例#1:
4 5 2 1 3 5 4 1 3 1 4 2 4 4 5 5 3 4 2 1 5 1 2 2 3 3 4 4 5 5 1 2 5 3 4 1 2 1 3 1 4 1 5 10 1 2 3 4 5 7 8 9 10 6 1 2 1 3 1 4 1 5 5 6 6 7 7 8 8 9 9 10
输出样例#1:
1 3 4 2 5 1 3 5 2 4 2 3 1 4 5 2 3 4 5 6 1 7 8 9 10
测试点编号 | $n \leq$ | 特殊性质 |
---|---|---|
$1 \sim 2$ | $10$ | 无 |
$3 \sim 4$ | $160$ | 树的形态是一条链 |
$5 \sim 7$ | $2000$ | 树的形态是一条链 |
$8 \sim 9$ | $160$ | 存在度数为 $n-1$ 的结点 |
$10 \sim 12$ | $2000$ | 存在度数为 $n-1$ 的结点 |
$13 \sim 16$ | $160$ | 无 |
$17 \sim 20$ | $2000$ | 无 |
由于作者水平有限,本题解表述、逻辑可能存在不足之处,欢迎读者以自身理解提出改进意见。
参考资料:[CSP-S2019]树上的数 – 仰望星空,脚踏实地 – 洛谷博客
刚看到这个题目的时候,总是想发掘树链上的性质,处理点上的信息,这种惯性思维使我完全没有思路。事实上,这个题处理边上的信息更好。
本题中,有 $25$ 分的菊花图部分分,在菊花图和链两个部分分中,菊花图事实上相对好思考一些,因此先考虑菊花图的情况。
假如我们已经得到了菊花图上删边的顺序为 $(1, u_1), (1, u_2), \dots, (1, u_m)$ ,则按顺序删边后,容易发现, $1$ 号点的数字移动至 $u_1$, $u_1$ 的数字移动至 $u_2$,……, $u_m$ 的数字移动至 $1$ 。
因此我们可以贪心地构造这个顺序,枚举数字 $1 \sim n$ ,每个数字贪心地选择删边顺序中的下一条边,该数字最后的位置就是该边对应的 $u_i$ 。
链的情况同样是 $25$ 分。
在链的情况中,分析边的关系是必要的。对于一个数字 $k$ 从初始位置 $u_1$ 移动至 $u_m$ ,在路径 $u_1, u_2, \dots, u_m$ 上有以下性质:
因此对于每个点可以获得一个删边的顺序,左先于右或右先于左。仍然按 $1 \sim n$ 的顺序枚举数字,检查每个数字从初始位置向左向右能走到的点中的最小编号。不能走仅当该点已确定的顺序不满足当前需要的顺序。
与链类似,对于一个数字 $k$ 从初始位置 $u_1$ 移动至 $u_m$ ,在路径 $u_1, u_2, \dots, u_m$ 上有以下性质:
容易发现,这些限制都是应用在某一点的边中的,因此可以单独考虑每个点的情况。依然是 $1 \sim n$ 枚举每个数字,从这个数字的初始位置开始 DFS ,检查路径上的点是否可以作为中间点/终点即可。
这里是这个题的实现中最难的位置,即检查是否满足中间点/终点的条件。这里,我使用了链表+并查集的实现方法管理边的关系。用类似链表的结构存储某个点的边是否被应用了在某边之后/之前被删的限制,用并查集存储某个点的边的限制连成的链式结构,且用两个数组 beg 和 end 存储某个点的边中,被固定为第一条/最后一条被删的边。
对于一个点,它能作为终点的条件为:
对于一个点,它能作为中间点的条件为:
根据以上条件进行判断一个点是否能作为中间点/终点,寻找每个数字的最小编号终点,并在路径上应用出入边的限制即可。
由于细节众多,文字描述无法包括所有方面,可以参考代码中的注释来理解。
UPD:被卡常了,开 O2 洛谷可过。
// Code by KSkun, 2019/11
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <vector>
#include <utility>
typedef long long LL;
typedef std::pair<int, int> PII;
inline char fgc() {
static char buf[100000], * p1 = buf, * p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
LL res = 0, neg = 1; char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
return res * neg;
}
inline char readsingle() {
char c;
while(!isgraph(c = fgc())) {}
return c;
}
const int MAXN = 2005;
int T, n, ptn[MAXN], deg[MAXN], beg[MAXN], end[MAXN]; // 每个点的第一条/最后一条被删的边
std::vector<int> gra[MAXN];
struct UnionFindSet {
int fa[MAXN];
bool pre[MAXN], nxt[MAXN]; // 一条边有无前后关系
void clear() {
for (int i = 1; i <= n; i++) fa[i] = i;
memset(pre, 0, sizeof(pre));
memset(nxt, 0, sizeof(nxt));
}
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void join(int x, int y) {
int fx = find(x), fy = find(y);
fa[fy] = fx;
nxt[x] = pre[y] = true;
}
bool sameset(int x, int y) {
return find(x) == find(y);
}
} ufs[MAXN];
int dfs(int u, int f) {
int mn = n + 1;
// 能作为终点的条件:不是起点;入边可以是最后删边;入边之后无必须删边;入边和最后删边不在一条关系链中(只剩一条链时除外)
if (f != 0 && (end[u] == 0 || end[u] == f) && !ufs[u].nxt[f] &&
!(beg[u] != 0 && deg[u] > 1 && ufs[u].sameset(f, beg[u]))) {
mn = std::min(mn, u);
}
for (int v : gra[u]) {
if (v == f) continue;
if (f == 0) {
// 不能作为起点之后的点的条件:起点的最后删边不是这条;这条边之前有必须删的边;这条边与最后删边在同一条关系链中,且仍有未加入关系链中的边
if (beg[u] != 0 && beg[u] != v) continue;
if (ufs[u].pre[v]) continue;
if (end[u] != 0 && deg[u] > 1 && ufs[u].sameset(v, end[u])) continue;
mn = std::min(mn, dfs(v, u));
} else {
// 不能作为中间点的条件:入边是最后删边;出边是最先删边;入边和出边已在同一条关系链中;出边之前有必须删边;入边之后有必须删边;应用出入边关系后让最先删边和最后删边在同一条关系链中,且有其他边未在该关系链中
if (f == end[u] || v == beg[u] || ufs[u].sameset(f, v)) continue;
if (ufs[u].pre[v] || ufs[u].nxt[f]) continue;
if (beg[u] != 0 && end[u] != 0 && deg[u] > 2 &&
ufs[u].sameset(f, beg[u]) && ufs[u].sameset(v, end[u])) continue;
mn = std::min(mn, dfs(v, u));
}
}
return mn;
}
bool dfs2(int u, int f, int& tar) {
if (u == tar) {
end[u] = f; return true;
}
for (int v : gra[u]) {
if (v == f) continue;
if (dfs2(v, u, tar)) {
if (f == 0) {
beg[u] = v;
} else {
ufs[u].join(f, v);
deg[u]--;
}
return true;
}
}
return false;
}
int main() {
T = readint();
while (T--) {
n = readint();
// init
memset(beg, 0, sizeof(beg));
memset(end, 0, sizeof(end));
memset(deg, 0, sizeof(deg));
for (int i = 1; i <= n; i++) {
gra[i].clear();
ufs[i].clear();
}
// input
for (int i = 1; i <= n; i++) ptn[i] = readint();
for (int i = 1, x, y; i < n; i++) {
x = readint(); y = readint();
gra[x].push_back(y);
gra[y].push_back(x);
deg[x]++; deg[y]++; // deg 表示一个点的边关系构成的链的数量,初始为度数,之后每加入一个关系就对其减 1
}
// process
for (int i = 1; i <= n; i++) {
int mn = dfs(ptn[i], 0);
dfs2(ptn[i], 0, mn);
printf("%d ", mn);
}
putchar('\n');
}
return 0;
}
题目地址:洛谷:【P2495】[SDOI2011]消耗战 – 洛谷、BZOJ:Problem 2286. — [Sdoi2011]消耗战
在一场战争中,战场由n个岛屿和n-1个桥梁组成,保证每两个岛屿间有且仅有一条路径可达。现在,我军已经侦查到敌军的总部在编号为1的岛屿,而且他们已经没有足够多的能源维系战斗,我军胜利在望。已知在其他k个岛屿上有丰富能源,为了防止敌军获取能源,我军的任务是炸毁一些桥梁,使得敌军不能到达任何能源丰富的岛屿。由于不同桥梁的材质和结构不同,所以炸毁不同的桥梁有不同的代价,我军希望在满足目标的同时使得总代价最小。
侦查部门还发现,敌军有一台神秘机器。即使我军切断所有能源之后,他们也可以用那台机器。机器产生的效果不仅仅会修复所有我军炸毁的桥梁,而且会重新随机资源分布(但可以保证的是,资源不会分布到1号岛屿上)。不过侦查部门还发现了这台机器只能够使用m次,所以我们只需要把每次任务完成即可。
给你一棵包含$n$个点的带边权的树,有$m$次询问,每次给你一个大小为$k_i$的点集,你可以花费边权代价切断树上的一些边,使得这个点集中的点与树根$1$不连通,求满足上述条件的最小切边代价。
输入格式:
第一行一个整数n,代表岛屿数量。
接下来n-1行,每行三个整数u,v,w,代表u号岛屿和v号岛屿由一条代价为c的桥梁直接相连,保证1<=u,v<=n且1<=c<=100000。
第n+1行,一个整数m,代表敌方机器能使用的次数。
接下来m行,每行一个整数ki,代表第i次后,有ki个岛屿资源丰富,接下来k个整数h1,h2,…hk,表示资源丰富岛屿的编号。
输出格式:
输出有m行,分别代表每次任务的最小代价。
输入样例#1:
10 1 5 13 1 9 6 2 1 19 2 4 8 2 3 91 5 6 8 7 5 4 7 8 31 10 7 9 3 2 10 6 4 5 7 8 3 3 9 4 6
输出样例#1:
12 32 22
【数据规模和约定】
对于10%的数据,2<=n<=10,1<=m<=5,1<=ki<=n-1
对于20%的数据,2<=n<=100,1<=m<=100,1<=ki<=min(10,n-1)
对于40%的数据,2<=n<=1000,m>=1,sigma(ki)<=500000,1<=ki<=min(15,n-1)
对于100%的数据,2<=n<=250000,m>=1,sigma(ki)<=500000,1<=ki<=n-1
如果只有一次询问,我们可以进行一次DFS处理出$mn[u]$代表$u$点到$1$点链上的最小边,显然,断开它是切断$u$和$1$连接的最优方案。我们用$dp[u]$表示切断$u$子树中所有点与$1$的连接的最小代价,则转移为
$$ dp[u] = \sum_{v \in \mathrm{son}(u)} \min \{ dp[v], mn[v] \} $$
这个转移表示要么切断$v$到$u$的这条边,要么切断其子树内的边。DP初值设置为对于每一个点集里的点,$dp[u]$初始为$mn[u]$,这样在DP求和的时候,如果一个父亲自己也是点集中的点,权值也会被计算进去。显然,如果两个点到$1$路径上的最优边是同一条,那么一定可以在这条最优边处向上转移时只计算一次权值,这是由于$\min$的存在。
然而这个算法是$O(n)$的,也就是说,总复杂度为$O(mn)$,并不能通过本题。
我们考虑建立虚树解决这个问题。虚树是指一棵只包含指定点即它们两两间LCA的辅助用树,这样做可以减少树上点的数量,降低DP的复杂度至$O(k_i \log k_i)$。
具体而言,如果我们处理出树的DFS序,那么对DFS序相邻的两点求LCA,一定可以得到所有点两两的LCA,这是由于,在不同子树内的点的LCA可以在处理到子树DFS序区间端点之间的LCA的时候被求出来。但是,LCA和儿子的边的关系并不明确,直接建边似乎是不明智的选择。我们考虑一种特殊的树的遍历序:欧拉序,它是指DFS遍历时,进入DFS栈的时候将该点加入序列,出栈时也加入一次。如果对于上面的所有点及其LCA的集合,分别插入出入栈点用欧拉序排序,就可以利用这一顺序来模拟DFS了,省去了建边的过程。
模拟DFS的时候,只需要在出栈的时候进行操作,这是因为,此时出栈点$u$子树的DP已经计算完毕,只需要去更新它父亲的DP值即可,而出栈后的栈顶元素就是它的父亲,直接按照上面的方程去转移即可。
使用欧拉序的虚树算法的复杂度是单次$O(n \log n)$的,达到这个复杂度的操作是查询LCA和排序。这里的$n$指的是虚树的节点数,它的规模是$O(k_i \log k_i)$的。
如此,总复杂度终于变得科学了不少。不过本题有点卡常,稍微卡一卡也可以卡进去。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <vector>
typedef long long LL;
inline LL min(LL a, LL b) {
return a < b ? a : b;
}
inline char fgc() {
static char buf[100000], *p1 = buf, *p2 = buf;
return p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 100000, stdin), p1 == p2)
? EOF : *p1++;
}
inline LL readint() {
register LL res = 0, neg = 1; register char c = fgc();
for(; !isdigit(c); c = fgc()) if(c == '-') neg = -1;
for(; isdigit(c); c = fgc()) res = res * 10 + c - '0';
return res * neg;
}
const int MAXN = 500005;
int n, m;
struct Edge {
int to, w;
};
std::vector<Edge> gra[MAXN];
int anc[MAXN][20], mn[MAXN], dep[MAXN], dfn[MAXN], clk;
void dfs(int u, int fa) {
dfn[u] = ++clk;
for(int i = 0; i < gra[u].size(); i++) {
int v = gra[u][i].to;
if(v == fa) continue;
anc[v][0] = u;
dep[v] = dep[u] + 1;
mn[v] = std::min(mn[u], gra[u][i].w);
dfs(v, u);
}
dfn[u + n] = ++clk;
}
inline void calanc() {
for(int i = 1; i <= 19; i++) {
for(int j = 1; j <= n; j++) {
anc[j][i] = anc[anc[j][i - 1]][i - 1];
}
}
}
inline int querylca(int u, int v) {
if(dep[u] > dep[v]) std::swap(u, v);
int del = dep[v] - dep[u];
for(int i = 19; i >= 0; i--) {
if(del & (1 << i)) v = anc[v][i];
}
if(u == v) return u;
for(int i = 19; i >= 0; i--) {
if(anc[u][i] != anc[v][i]) {
u = anc[u][i];
v = anc[v][i];
}
}
return anc[u][0];
}
inline bool cmp(int a, int b) {
return dfn[a] < dfn[b];
}
LL dp[MAXN];
bool vis[MAXN];
std::vector<int> vec;
int sta[MAXN], stop;
int main() {
n = readint();
for(int i = 1, u, v, w; i < n; i++) {
u = readint(); v = readint(); w = readint();
gra[u].push_back(Edge {v, w});
gra[v].push_back(Edge {u, w});
}
mn[1] = 1e9;
dfs(1, 0);
calanc();
m = readint();
while(m--) {
vec.clear();
int k = readint();
for(int i = 1; i <= k; i++) {
int t = readint();
vis[t] = true; dp[t] = mn[t]; vec.push_back(t);
}
std::sort(vec.begin(), vec.end(), cmp);
for(int i = 1; i < k; i++) {
int l = querylca(vec[i - 1], vec[i]);
if(!vis[l]) {
vis[l] = true;
vec.push_back(l);
}
}
if(!vis[1]) vec.push_back(1);
for(int i = 0; vec[i] <= n; i++) {
vec.push_back(vec[i] + n);
}
std::sort(vec.begin(), vec.end(), cmp);
for(int i = 0; i < vec.size(); i++) {
if(vec[i] <= n) {
sta[stop++] = vec[i];
} else {
int u = sta[--stop];
if(u != 1) {
int fa = sta[stop - 1]; dp[fa] += min(mn[u], dp[u]);
} else {
printf("%lld\n", dp[u]);
}
dp[u] = 0; vis[u] = false;
}
}
}
return 0;
}
题目地址:洛谷:【P2414】[NOI2011]阿狸的打字机 – 洛谷、BZOJ:Problem 2434. — [Noi2011]阿狸的打字机
阿狸喜欢收藏各种稀奇古怪的东西,最近他淘到一台老式的打字机。
打字机上只有28个按键,分别印有26个小写英文字母和’B’、’P’两个字母。经阿狸研究发现,这个打字机是这样工作的:
例如,阿狸输入aPaPBbP,纸上被打印的字符如下:
a aa ab
我们把纸上打印出来的字符串从1开始顺序编号,一直到n。打字机有一个非常有趣的功能,在打字机中暗藏一个带数字的小键盘,在小键盘上输入两个数(x,y)(其中1≤x,y≤n),打字机会显示第x个打印的字符串在第y个打印的字符串中出现了多少次。
阿狸发现了这个功能以后很兴奋,他想写个程序完成同样的功能,你能帮助他么?
你有一个打字机,打字机支持三种操作
纸上会被印上若干行文字,现在你有$m$组询问,每组询问求第$x$行的字符串在第$y$行的字符串中出现了几次。
输入格式:
输入的第一行包含一个字符串,按阿狸的输入顺序给出所有阿狸输入的字符。
第二行包含一个整数m,表示询问个数。
接下来m行描述所有由小键盘输入的询问。其中第i行包含两个整数x, y,表示第i个询问为(x, y)。
输出格式:
输出m行,其中第i行包含一个整数,表示第i个询问的答案。
输入样例#1:
aPaPBbP 3 1 2 1 3 2 3
输出样例#1:
2 1 0
数据范围:
对于100%的数据,n<=100000,m<=100000,第一行总长度<=100000。
首先我们可以对所有串建一个AC自动机,由于上面的操作的一些特殊性质,其实可以一位一位地边操作边插入到Trie树中。
处理每个询问的时候都在AC自动机上跑匹配不现实,会TLE。我们需要一种优化到$O(\log n)$回答单次询问的方法。我们想到了fail树,问$y$中有几个$x$等价于问fail树中$x$的末结点子树中有几个属于$y$路径上的节点。
我们考虑把fail树建出来,处理DFS序,对DFS序建一个树状数组。我们还是按照读入操作的那个顺序去遍历这个Trie树,每到一个节点对树状数组中这个节点DFS序的位置+1,退出一个节点就-1,这样,当到达每个串的结尾节点时,它的Trie树上的树链节点都被+1了,此时对于这个串作为$y$的询问就可以回答了,直接在树状数组上查$x$的子树和就好。
因此,思路是,离线处理这些询问,把询问按$y$分类,每遍历到一个字符串结尾的时候回答该串作为$y$的询问,这样复杂度就是很靠谱的$O(n \log n)$了。
// Code by KSkun, 2018/7
#include <cstdio>
#include <cctype>
#include <cstring>
#include <algorithm>
#include <vector>
#include <queue>
const int MAXN = 100005;
char op[MAXN];
int n, m, ans[MAXN];
typedef std::pair<int, int> PII;
std::vector<PII> vec[MAXN];
int ch[MAXN][26], fa[MAXN], fail[MAXN], end[MAXN], tot, stot;
std::vector<int> gra[MAXN];
inline void insert() {
int p = 0;
for(int i = 1; i <= n; i++) {
if(op[i] == 'P') {
end[++stot] = p;
} else if(op[i] == 'B') {
p = fa[p];
} else {
int c = op[i] - 'a';
if(!ch[p][c]) {
ch[p][c] = ++tot;
fa[tot] = p;
}
p = ch[p][c];
}
}
}
std::queue<int> que;
inline void calfail() {
for(int i = 0; i < 26; i++) {
if(ch[0][i]) {
gra[0].push_back(ch[0][i]);
que.push(ch[0][i]);
}
}
while(!que.empty()) {
int u = que.front(); que.pop();
for(int i = 0; i < 26; i++) {
if(ch[u][i]) {
int p = fail[u];
while(p && !ch[p][i]) p = fail[p];
fail[ch[u][i]] = ch[p][i];
gra[ch[p][i]].push_back(ch[u][i]);
que.push(ch[u][i]);
}
}
}
}
int dfn[MAXN], siz[MAXN], clk;
void dfs(int u) {
dfn[u] = ++clk; siz[u] = 1;
for(int i = 0; i < gra[u].size(); i++) {
int v = gra[u][i];
dfs(v);
siz[u] += siz[v];
}
}
int tr[MAXN];
inline int lowbit(int x) {
return x & -x;
}
inline void add(int x, int v) {
for(int i = x; i <= clk; i += lowbit(i)) {
tr[i] += v;
}
}
inline int query(int x) {
int res = 0;
for(int i = x; i; i -= lowbit(i)) {
res += tr[i];
}
return res;
}
inline void solve() {
int p = 0, cnt = 0;
for(int i = 1; i <= n; i++) {
if(op[i] == 'P') {
cnt++;
for(int j = 0; j < vec[cnt].size(); j++) {
int x = vec[cnt][j].first;
ans[vec[cnt][j].second] += query(dfn[end[x]] + siz[end[x]] - 1) - query(dfn[end[x]] - 1);
}
} else if(op[i] == 'B') {
add(dfn[p], -1);
p = fa[p];
} else {
int c = op[i] - 'a';
p = ch[p][c];
add(dfn[p], 1);
}
}
}
int main() {
scanf("%s%d", op + 1, &m); n = strlen(op + 1);
for(int i = 1, x, y; i <= m; i++) {
scanf("%d%d", &x, &y);
vec[y].push_back(PII(x, i));
}
insert();
calfail();
dfs(0);
solve();
for(int i = 1; i <= m; i++) {
printf("%d\n", ans[i]);
}
return 0;
}