【HNOI 2019】校园旅行
Problem#
Description#
某学校的每个建筑都有一个独特的编号。一天你在校园里无聊,决定在校园内随意地漫步。
你已经在校园里呆过一段时间,对校园内每个建筑的编号非常熟悉,于是你情不自禁的把周围每个建筑的编号都记了下来——但其实你没有真的记下来,而是把每个建筑的编号除以 2 取余数得到 0 或 1,作为该建筑的标记,多个建筑物的标记连在一起形成一个 01 串。
你对这个串很感兴趣,尤其是对于这个串是回文串的情况,于是你决定研究这个问题。
学校可以看成一张图,建筑是图中的顶点,而某些顶点之间存在无向边。对于每个顶点我们有一个标记(0 或者 1)。每次你会选择图中两个顶点,你想知道这两个顶点之间是否存在一条路径使得路上经过的点的标记形成一个回文串。
一个回文串是一个字符串使得它逆序之后形成的字符串和它自己相同,比如 010,1001 都是回文串,而 01,110 不是。注意长度为 1 的串总是回文串,因此如果询问的两个顶点相同,这样的路径总是存在。此外注意,经过的路径不一定为简单路径,也就是说每条边每个顶点都可以经过任意多次。
Input Format#
第一行三个整数 n,m,q,表示图中的顶点数和边数,以及询问数。
第二行为一个长度为 n 的 01 串,其中第 n 个字符表示第 i 个顶点(即顶点 i)的标记,点从 1 开始编号。
接下来 m 行,每一行是两个整数 ui,vi,表示顶点 ui 和顶点 vi 之间有一条无向边,不存在自环或者重边。
接下来 q 行,每一行存在两个整数 xi,yi,表示询问顶点 xi 和顶点 yi 的点之间是否有一条满足条件的路径。
Output Format#
输出 q 行,每行一个字符串 YES
,或者 NO
。输出 YES
表示满足条件的路径存在,输出 NO
表示不存在。
Sample#
Input 1#
5 4 2
00010
4 5
1 3
4 2
2 5
3 5
1 3
Output 1#
NO
YES
Input 2#
10 11 10
0011011111
4 6
10 6
5 9
4 7
10 7
5 8
1 9
5 7
1 10
5 1
5 6
10 3
7 4
8 10
9 4
8 9
6 6
2 2
9 9
10 9
3 4
Output 2#
NO
YES
YES
NO
YES
YES
YES
YES
YES
NO
Explanation#
Explanation for Input 1#
对于第一个询问,3 号点和 2 号点不连通, 因此答案为 NO
。
对于第二个询问,一条合法的路径是 1→3,路径上的标号形成的字符串为 00。注意合法路径不唯一。
Range#
对于 30% 的数据,1≤m≤104;
对于 70% 的数据,1≤n≤3×103,1≤m≤5×104;
对于 100% 的数据,1≤n≤5×103,1≤m≤5×105,1≤q≤105。
Algorithm#
DP
Mentality#
考场上无人切的神题 orz ,myy nb 。
由于 n 异常的小,所以我们发现完全可以用 n2 的二维空间来储存信息,而 m 和 q 相对来说又异常大,这启发我们用一种看起来很暴力的方法做这道题 -- 预处理出所有点对的情况。
30 分的做法还是很好想的,我们发现可以将回文路径分为两类:长度为奇数的,长度为偶数的。
设 f[i][j] 为 i,j 之间是否有回文路径,那么我们先处理出长度最短的奇偶回文路径。首先,长度最短的奇数回文路径就是每个点自己,即 f[i][i]=1 。然后观察到对于每条边,如果连的两个点 u,v 编号相同,则 f[u][v]=f[v][u]=1 ,这些就是长度最短的偶数回文路径。
然后考虑利用这些信息进行 DP 转移,我们用 bfs 的顺序来转移即可。
将这些两点之间有路径的二元组 (u,v) 扔进队列,转移的时候枚举 u,v 的出边 tou,tov,如果 tou 的编号与 tov 相同,那么 tou,tov 之间肯定也存在回文路径,我们将 f 数组更新,然后将二元组 tou,tov 丢入队列末尾等待下一次转移即可。
由于每次转移都要枚举两点的出边一一判断,所以复杂度为 m2 。
代码大概长这个样子?
while(h<t)
{
h++;
for(int i=hd[u[h]];i;i=Nx[i])
for(int j=hd[v[h]];j;j=Nx[j])
if(S[To[i]]==S[To[j]]&&!f[To[i]][To[j]])
f[To[i]][To[j]]=f[To[j]][To[i]]=1,Add(To[i],To[j]);
}
由于 STL 常数太大,所以手写队列 (也就总共 5e7 的空间而已)
询问一次就直接看它的 f 数组即可。
接下来考虑 100 分做法。
观察到 m 巨大,我们考虑减少边的枚举。
我们将所有转移分成两类:向相同编号的点转移,向不同编号的点转移。
那么我们也就可以依此将边分为两类:连接相同编号点的边,连接不同编号点的边。
我们先考虑一类边,譬如连接相同编号点的边。
这些边把图分成了许多个联通块,我们发现,对于一个联通块内的转移,只取决于一件事:这个联通块是不是个二分图。
为什么呢?我们来考虑一下,如果联通块是一个二分图,那么它满足两个性质。
-
能将联通块内的点划分成两个集合,同一集合内的点互不直接相连。
-
同时由于这是个联通块,两点之间皆可达。
那么不难发现,如果我在一个集合内,想要转移到集合内另一点,必定会经过偶数条边。
因为我到达这个点的过程中,注定只能是重复 当前集合
-> 另一集合
-> 当前集合
这样的步骤,所以最后的步数一定是偶数条。
换而言之,若联通块为二分图,那么联通块内任意两点之间的路径长度奇偶性唯一。
而注意到,当我们 DP 转移的时候,若回文串两端新增的数字全都相同,譬如在左右端都添上 0 ,那么我们只需要保证左右两边新增的数量相同即可。
而根据题目的性质可知,我们为保证数量相等,完全可以在一条连向一个相同编号点的边上来回横走保证数量的增值。但是这样不改变奇偶性。
那么奇偶性就成了判断 DP 转移的重要性质了。
接着上面的推论,由于若联通块为二分图,那么联通块内两点件路径长度奇偶性唯一。那么我们在这个联通块内,只需要保留一颗生成树即可,因为 奇偶唯一 ,所以 不影响 DP 过程 。
然后我们再来看,如果不是二分图怎么办。还是划分成两个集合,那么同一集合内至少有一对点 (u,v) 之间直接有连边。由于只考虑连接两个不同集合的边时,从 u 至 v 必定有一条长度为 偶数 的路径,所以再加上一条边,这个联通块内就有了一个 奇环 。则联通块内任意一点都可以走到奇环上通过绕环改变路径长度奇偶性。
那么不难发现,我们只需要先像二分图一样,保留一颗生成树。至于那个奇环,我们只需要在生成树内任意一个点上随便连个自环就行了 QwQ,反正只是要个奇环而已,自环当然也是啦。
以上是连接相同编号点的边的处理方式。
至于连接不同编号的边的话,我们发现这联通块肯定就是个二分图,那么直接保留生成树即可。
那么边数减少为 n 的级别,此时再去 DP ,复杂度就降为 n2 了。
如若未懂详见代码
Code#
#include <cstdio>
#include <iostream>
using namespace std;
int n, m, Q;
int cntr, head[5001], nx[1000001], to[1000001], col[5001];
int cr, hd[5001], Nx[1000001], To[1000001];
int h, t, u[25000001], v[25000001];
bool flag, f[5001][5001];
char S[5001];
struct node {
int u, v;
};
void read(int &x) {
x = 0;
char ch = getchar();
while (!isdigit(ch)) ch = getchar();
while (isdigit(ch)) x = x * 10 + ch - '0', ch = getchar();
}
void addr(int u, int v) {
cntr++;
nx[cntr] = head[u], to[cntr] = v;
head[u] = cntr;
}
void Addr(int u, int v) {
cr++;
Nx[cr] = hd[u], To[cr] = v;
hd[u] = cr;
}
void Add(int U, int V) {
t++;
u[t] = U, v[t] = V;
}
void dye(int x, bool type) {
for (int i = head[x]; i; i = nx[i]) {
int p = to[i];
if ((S[p] == S[x]) == type) //构图参数的使用
{
if (col[p] != -1)
if (!(col[p] ^ col[x])) flag = 1; //染色冲突则不是二分图
if (col[p] == -1) {
col[p] = col[x] ^ 1, dye(p, type);
Addr(x, p), Addr(p, x); //加边
}
}
}
}
void Read_Init() {
read(n), read(m), read(Q);
scanf("%s", S + 1);
int u, v;
while (m--) {
read(u), read(v);
if (S[u] == S[v])
f[u][v] = f[v][u] = 1, Add(u, v); //先加入偶数最短边,并更新 DP 数组
addr(u, v), addr(v, u);
}
}
void Build_Init() {
for (int i = 1; i <= n; i++)
f[i][i] = 1, Add(i, i); //加入奇数最短边,并更新 DP 数组
for (int k = 0; k < 2; k++) // k 是构图参数
{
for (int i = 1; i <= n; i++) col[i] = -1; //二分图染色初始化
for (int i = 1; i <= n; i++)
if (col[i] == -1) {
flag = 0;
col[i] = 0, dye(i, k);
if (flag) Addr(i, i); //如果不是二分图那就加个自环
}
}
}
void DP() {
while (h < t) {
h++;
for (int i = hd[u[h]]; i; i = Nx[i])
for (int j = hd[v[h]]; j; j = Nx[j])
if (S[To[i]] == S[To[j]] && !f[To[i]][To[j]])
f[To[i]][To[j]] = f[To[j]][To[i]] = 1, Add(To[i], To[j]); // DP 转移
}
}
void Answer() {
int u, v;
while (Q--) {
read(u), read(v);
if (f[u][v])
printf("YES\n");
else
printf("NO\n");
}
}
int main() {
Read_Init();
Build_Init();
DP();
Answer();
}
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Java 中堆内存和栈内存上的数据分布和特点
· 开发中对象命名的一点思考
· .NET Core内存结构体系(Windows环境)底层原理浅谈
· C# 深度学习:对抗生成网络(GAN)训练头像生成模型
· .NET 适配 HarmonyOS 进展
· 本地部署 DeepSeek:小白也能轻松搞定!
· 如何给本地部署的DeepSeek投喂数据,让他更懂你
· 从 Windows Forms 到微服务的经验教训
· 李飞飞的50美金比肩DeepSeek把CEO忽悠瘸了,倒霉的却是程序员
· 超详细,DeepSeek 接入PyCharm实现AI编程!(支持本地部署DeepSeek及官方Dee