洛谷·仓鼠找sugar

初见安~这里是传送门:洛谷P3398

题目描述

小仓鼠的和他的基(mei)友(zi)sugar住在地下洞穴中,每个节点的编号为1~n。地下洞穴是一个树形结构。这一天小仓鼠打算从从他的卧室(a)到餐厅(b),而他的基友同时要从他的卧室(c)到图书馆(d)。他们都会走最短路径。现在小仓鼠希望知道,有没有可能在某个地方,可以碰到他的基友?

小仓鼠那么弱,还要天天被zzq大爷虐,请你快来救救他吧!

输入格式

第一行两个正整数n和q,表示这棵树节点的个数和询问的个数。

接下来n-1行,每行两个正整数u和v,表示节点u到节点v之间有一条边。

接下来q行,每行四个正整数a、b、c和d,表示节点编号,也就是一次询问,其意义如上。

输出格式

对于每个询问,如果有公共点,输出大写字母“Y”;否则输出“N”。

输入输出样例

输入 #1

5 5
2 5
4 2
1 3
1 4
5 1 5 1
2 2 1 4
4 1 3 4
3 1 1 5
3 5 1 4

输出 #1复制

Y
N
Y
Y
Y

说明/提示

__本题时限1s,内存限制128M,因新评测机速度较为接近NOIP评测机速度,请注意常数问题带来的影响。__

20%的数据 n<=200,q<=200

40%的数据 n<=2000,q<=2000

70%的数据 n<=50000,q<=50000

100%的数据 n<=100000,q<=100000

题解

说白了就是让你判断两条路径是否相交。这多大点儿事儿啊!直接树剖+线段树给第一条路径打标记看第二条是否涉及到有标记的点就行了呗!!!!【看官冷静……还有后文的……】

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 100005
using namespace std;
typedef long long ll;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

int n, Q;
struct edge {
	int to, nxt;
	edge() {}
	edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];

int head[maxn], k = 0;
void add(int u, int v) {e[k] = edge(v, head[u]); head[u] = k++;}

int son[maxn], top[maxn], size[maxn], dep[maxn], fa[maxn], dfn[maxn], tot = 0;
bool tree[maxn << 2];
struct HLD {
	void dfs1(int u) {
		size[u] = 1;
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to; if(v == fa[u]) continue;
			dep[v] = dep[u] + 1;  fa[v] = u;
			dfs1(v); size[u] += size[v];
			if(size[son[u]] < size[v]) son[u] = v;
		}
	}
	
	void dfs2(int u, int tp) {
		top[u] = tp, dfn[u] = ++tot; if(son[u]) dfs2(son[u], tp);
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to;
			if(v != fa[u] && v != son[u]) dfs2(v, v);
		}
	}
	
	void change(int p, int l, int r, int ls, int rs) {//区间标记 
		if(ls <= l && r <= rs) {tree[p] = true; return;}
		int mid = l + r >> 1;
		if(ls <= mid) change(p << 1, l, mid, ls, rs);
		if(rs > mid) change(p << 1 | 1, mid + 1, r, ls, rs);
		tree[p] = tree[p << 1] | tree[p << 1 | 1];
	}
	
	void flag(int u, int v) {//lca打标记 
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);
			change(1, 1, n, dfn[top[v]], dfn[v]);
			v = fa[top[v]];
		}
		if(dep[u] > dep[v]) swap(u, v);
		change(1, 1, n, dfn[u], dfn[v]);
	}
	
	bool ask(int p, int l, int r, int ls, int rs) {//查询是否有标记 
		if(ls <= l && r <= rs) return tree[p];
		int mid = l + r >> 1, ans = 0;
		if(!tree[p << 1] && !tree[p << 1 | 1]) tree[p << 1] |= tree[p], tree[p << 1 | 1] |= tree[p];
		//上面这句判断两区间是否都没有标记很重要 
		if(ls <= mid) ans |= ask(p << 1, l, mid, ls, rs);
		if(rs > mid) ans |= ask(p << 1 | 1, mid + 1, r, ls, rs);
		return ans;
	}
	
	bool check(int u, int v) {//检查是否有覆盖到路径1的标记 
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);			
			if(ask(1, 1, n, dfn[top[v]], dfn[v])) {return true;}
			v = fa[top[v]];
		}
		
		if(dep[u] > dep[v]) swap(u, v);
		if(ask(1, 1, n, dfn[u], dfn[v])) return true;
		return false;
	}
}H;

signed main() {
	memset(head, -1, sizeof head);
	n = read(), Q = read();
	for(int u, v, i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u);

	H.dfs1(1), H.dfs2(1, 1);
	register int s1, s2, t1, t2;
	while(Q--) {
		s1 = read(), t1 = read(), s2 = read(), t2 = read();
		memset(tree, 0, sizeof tree);//线段树 
		if(s1 == s2 || t1 == t2) {puts("Y"); continue;}
		H.flag(s1, t1);//给第一条路径打标记 
		if(H.check(s2, t2)) puts("Y");
		else puts("N");
	}
	return 0;
}

然后——这就是在TLE的边缘试探啊。

所以我想了想——一定是那个memset耗时太长。所以我们只有舍弃线段树往正解考虑了。【大雾

如果两条路径有交集,那么其中一个的LCA一定在另一个的路径上

这条结论比较好证明——主要是抓住树形结构从下往上是形如合并的关系就很好理解了,可以手动举一些例子来看。

那么我是不是直接看【树剖思想中】其中一个的lca在线段树上的dfn值【就是重链剖分后的序】是不是存在于另一条路径的某个区间内不就行了?

主要是这样就可以去掉memset了……瞬间就可以快很多。

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 100005
using namespace std;
typedef long long ll;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

int n, Q;
struct edge {
	int to, nxt;
	edge() {}
	edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];

int head[maxn], k = 0;
void add(int u, int v) {e[k] = edge(v, head[u]); head[u] = k++;}

int son[maxn], top[maxn], size[maxn], dep[maxn], fa[maxn], dfn[maxn], tot = 0;
bool tree[maxn << 2];
struct HLD {
	void dfs1(int u) {
		size[u] = 1;
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to; if(v == fa[u]) continue;
			dep[v] = dep[u] + 1;  fa[v] = u;
			dfs1(v); size[u] += size[v];
			if(size[son[u]] < size[v]) son[u] = v;
		}
	}
	
	void dfs2(int u, int tp) {
		top[u] = tp, dfn[u] = ++tot; if(son[u]) dfs2(son[u], tp);
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to;
			if(v != fa[u] && v != son[u]) dfs2(v, v);
		}
	}
	
	int LCA(int u, int v) {//求LCA 
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);			
			v = fa[top[v]];
		}
		
		if(dep[u] > dep[v]) swap(u, v);
		return u;	
	}
	
	bool check(int lca, int u, int v) {//检验是否在路径上 
		lca = dfn[lca];
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);
			if(dfn[top[v]] <= lca && lca <= dfn[v]) return true;			
			v = fa[top[v]];
		}
		
		if(dep[u] > dep[v]) swap(u, v);
		if(dfn[u] <= lca && lca <= dfn[v]) return true;
		return false;
	}
}H;

signed main() {
	memset(head, -1, sizeof head);
	n = read(), Q = read();
	for(int u, v, i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u);

	H.dfs1(1), H.dfs2(1, 1);
	register int s1, s2, t1, t2;
	while(Q--) {
		s1 = read(), t1 = read(), s2 = read(), t2 = read();
		if(s1 == s2 || t1 == t2) {puts("Y"); continue;}
		int lca1 = H.LCA(s1, t1), lca2 = H.LCA(s2, t2);	
			
		if(H.check(lca1, s2, t2)) puts("Y");//检查
		else if(H.check(lca2, s1, t1)) puts("Y");
		else puts("N");
	}
	return 0;
}

这样一来就可以快很多了,加上树剖求LCA本就比倍增块,所以总耗时就只有200+ms

当然,再让步骤简单一点儿,总时间可以快5ms【???????

也就是说我们还要得出一个结论。

在刚刚那条结论的基础上,我们继续展开——既然其中一个的LCA一定在另一条的路径上,也就是说这个LCA一定在另一条路径的起点或者终点到其LCA的路径上。而因为树的形态,我们可以忽略起点和终点的概念,就当是两个点,理解为路径不管是折链还是直链我们都要先让下面那个点往上走。所以我们需要我们具体一点:假设有两条路径,s1->t1, s2->t2,其LCA分别为lca1,lca2,并且有dep[lca1]>dep[lca2],那么当且仅当lca1在路径s2->lca2或者t2->lca2的时候两条路径有公共部分。换言之,lca1要么在s2的上方,要么在t2的上方并且不超过lca2。也就是dep[lca1] == dep[LCA(s2, lca1)] 或者dep[lca1] == dep[LCA(t2, lca1)]

这样解的话就和树剖的知识没有任何关系了,直接求解LCA即可。

因为本狸比较懒,所以最后求LCA的方法还是树剖【大雾。主要看核心代码啦~

#include<algorithm>
#include<iostream>
#include<cstring>
#include<cstdio>
#include<cmath>
#include<queue>
#define maxn 100005
using namespace std;
typedef long long ll;
int read() {
	int x = 0, f = 1, ch = getchar();
	while(!isdigit(ch)) {if(ch == '-') f = -1; ch = getchar();}
	while(isdigit(ch)) x = (x << 1) + (x << 3) + ch - '0', ch = getchar();
	return x * f;
}

int n, Q;
struct edge {
	int to, nxt;
	edge() {}
	edge(int tt, int nn) {to = tt, nxt = nn;}
}e[maxn << 1];

int head[maxn], k = 0;
void add(int u, int v) {e[k] = edge(v, head[u]); head[u] = k++;}

int son[maxn], top[maxn], size[maxn], dep[maxn], fa[maxn], dfn[maxn], tot = 0;
bool tree[maxn << 2];
struct HLD {
	void dfs1(int u) {
		size[u] = 1;
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to; if(v == fa[u]) continue;
			dep[v] = dep[u] + 1;  fa[v] = u;
			dfs1(v); size[u] += size[v];
			if(size[son[u]] < size[v]) son[u] = v;
		}
	}
	
	void dfs2(int u, int tp) {
		top[u] = tp, dfn[u] = ++tot; if(son[u]) dfs2(son[u], tp);
		for(int i = head[u]; ~i; i = e[i].nxt) {
			register int v = e[i].to;
			if(v != fa[u] && v != son[u]) dfs2(v, v);
		}
	}
	
	int LCA(int u, int v) {
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);			
			v = fa[top[v]];
		}
		
		if(dep[u] > dep[v]) swap(u, v);
		return u;	
	}
	
	bool check(int lca, int u, int v) {
		lca = dfn[lca];
		while(top[u] != top[v]) {
			if(dep[top[u]] > dep[top[v]]) swap(u, v);
			if(dfn[top[v]] <= lca && lca <= dfn[v]) return true;			
			v = fa[top[v]];
		}
		
		if(dep[u] > dep[v]) swap(u, v);
		if(dfn[u] <= lca && lca <= dfn[v]) return true;
		return false;
	}
}H;

signed main() {
	memset(head, -1, sizeof head);
	n = read(), Q = read();
	for(int u, v, i = 1; i < n; i++) u = read(), v = read(), add(u, v), add(v, u);

	H.dfs1(1), H.dfs2(1, 1);
	register int s1, s2, t1, t2;
	while(Q--) {
		s1 = read(), t1 = read(), s2 = read(), t2 = read();
		if(s1 == s2 || t1 == t2) {puts("Y"); continue;}
		int lca1 = H.LCA(s1, t1), lca2 = H.LCA(s2, t2);		
		if(max(dep[s1], dep[t1]) < dep[lca2] || max(dep[s2], dep[t2]) < dep[lca1]) puts("N");
        //这里预判无解很重要,避免被正解判定判错
		else {
			if(dep[lca1] < dep[lca2]) swap(lca1, lca2), swap(s1, s2), swap(t1, t2);
            //这里我们就直接用lca1,所以如果深度不符合要求要先交换
			if(dep[lca1] == dep[H.LCA(lca1, s2)] || dep[lca1] == dep[H.LCA(lca1, t2)]) puts("Y");
			else puts("N");
		}
		
	}
	return 0;
}

以上就是全部内容啦!!!!!!!!!!!

迎评:)
——End——

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值