图论相关问题

图论相关问题

A. P2662 牛场围栏

同余最短路的板题,会了就没什么。见这篇

B. P4211 [LNOI2014] LCA

感觉离线处理的技巧得多加磨练。

这题的暴力显然就是 O(nm)O(nmlogn),暴力枚举 [l,r]z 的 LCA 的 dep 值,计算即可。

那么正解要么就是一次求出多个 LCA,要么就是把 dep 进行某种神奇的转化。

前者不太可做,后者还是可以想想的。

考虑将 depx 转化为 x 到根节点的节点个数。于是查询 depLCA(u,v) 就可以转化为:将 u 到根节点的路径上的点权值 +1,然后查询 v 到根节点的路径上的权值和。那么对于多个点也是同理的。

区间加、区间查,不是树剖还能是什么?不过我们不能直接暴力区间加和区间查,那样复杂度还是 O(nmlogn) 的。发现我们是要将 [l,r] 的节点全部区间加,进而想到差分,也就是把 l1r 单拎出来,一个删一个加。

但是这样貌似无法做到在线维护了,考虑离线。把一次询问拆成一个 [0,l1] 和一个 [0,r],全搞到一个结构体里面按照节点排序,然后扫一遍,用一个指针把区间加更新到当前点的位置,然后再判断当前点的种类,如果是要删的点就从答案中减去,否则加到答案上。

主要难点在于转化,以及想到差分。能把这种问题差分以及离线处理的思想是需要练习的。

struct Ask{
	int id,pos,z,k;
	bool operator<(const Ask&x)const{
		return pos<x.pos;
	}
}q[MAXN<<1];
ll ans[MAXN];

int main(){
	n=read(),m=read();
	for(int i=2,f;i<=n;++i){
		f=read()+1;
		addedge(f,i),addedge(i,f);
	}
	dfs(1,0);
	dfs2(1,1);
	for(int i=1,l,r,z;i<=m;++i){
		l=read()+1,r=read()+1,z=read()+1;
		q[(i<<1)-1]={i,l-1,z,-1};
		q[i<<1]={i,r,z,1};
	}
	sort(q+1,q+(m<<1|1));
	for(int i=1,j=1;i<=m<<1;++i){
		while(j<=q[i].pos) qadd(1,j++,1);
		if(~q[i].k) ans[q[i].id]+=qsum(1,q[i].z);
		else ans[q[i].id]-=qsum(1,q[i].z);
	}
	for(int i=1;i<=m;++i) write(ans[i]%MOD);
	return fw,0;
}

C. P1852 跳跳棋

神仙建模题,充分彰显了人类智慧

设三点从小到大为 x,y,z,则不论怎么跳都有且只有两种情况:

  • y 向两边跳
  • xz 向中间跳

前者可以无限跳,但后者只能跳有限次数!因为一旦 dis(x,y)=dis(y,z),那么 xz 都不能再向中间跳了,否则不合法。

反之,由 dis(x,y)=dis(y,z) 这种状态可以导出无限多种状态,这不由得我们考虑将这个状态设为一棵树的根节点,那么别的状态都是它的子孙。为了方便,我们将这个状态定义为 “初始状态”。

试着推广我们这个构想,对于任何一种状态,都能导出无限的状态,设其中两种分别为 a,b,那么从 a 变到 b 的最少步数是不是就是它们在这棵树上先变到它们的 LCA,再从 LCA 变到另外一种状态?

有没有一种茅厕顿开的感觉!

别急,我们一步一步走。现在我们已经成功地将其转化为一个树上问题,还有几个问题亟待解决。首先,思考如何判断两个点之间不可达。其实是容易的,我们只要找到两个状态的初始状态,如果它们的初始状态不同,那么他俩就是不可达的。

但是又引出另外一个问题:如何找初始状态?值域在 109,显然是不能暴力搜的。不过我们可以试着想,就算这三个数是 1,2,109 这种变态,我们实际上也可以一步转移,因为把 x,y,z 这种状态翻转到 y,x,z 这种状态时,x,y 两点间距离不变,事实上从相对位置来看可以看作直接平移。所以刚才那个变态最终转移到 1092,1091,109 我们实际上一步就能算出是走了 dis(y,z)1dis(x,y) 步,1 是为了防止最后出现两点重合的情况。至此我们找到初始状态的复杂度应该就是 log 级别了。

到这里,我们应该就能很快地判断出两个状态可不可达。接下来的任务是找出两个可达的点的最小步数。

显然,我们应该找两点的 LCA。但是显然什么倍增树剖都没法用,注意到刚才我们找初始状态的那个过程是可以限制步数的,所以我们应该可以通过找每一个状态向上跳父亲的次数来找 LCA,因为找到 LCA 之后再往上跳必定是相同节点,具有单调性。所以二分步数即可。注意在此之前应当先将 a,b 中较深的一个跳到和另外一个同样深,然后再执行二分。最后的答案就是二分出的答案 ×2 再加上较深的那个点预先跳的步数。

捋一下思路:先找到 a,b 状态各自的初始状态,同时记录跳的步数。如果初始状态不同直接 NO;否则,将步数比较大的那一个先跳到和另外一个相同,然后二分找出两点的 LCA,最后答案如同上文统计即可。

#include<bits/stdc++.h>
using namespace std;

struct Node{
	int x,y,z;
	bool operator!=(const Node&b)const{
		return x!=b.x||y!=b.y||z!=b.z;
	}
	void read(){
		scanf("%d%d%d",&x,&y,&z);
		if(x>y) swap(x,y);
		if(x>z) swap(x,z);
		if(y>z) swap(y,z);
	}
}a,b;

Node getrt(Node t,int s,int&step){
	int k=0;
	for(step=0;s;step+=k){
		int d1=t.y-t.x,d2=t.z-t.y;
		if(d1==d2) return t;
		else if(d1>d2){
			k=min((d1-1)/d2,s);
			t.y-=k*d2,t.z-=k*d2;
			s-=k;
		}else{
			k=min((d2-1)/d1,s);
			t.x+=k*d1,t.y+=k*d1;
			s-=k;
		}
	}
	return t;
}

int main(){
	a.read(),b.read();
	int sa,sb,tmp; // a到根距离,b到根距离 
	Node rta=getrt(a,2e9,sa),rtb=getrt(b,2e9,sb);
	if(rta!=rtb) return puts("NO"),0;
	if(sa<sb) swap(sa,sb),swap(a,b);
	a=getrt(a,sa-sb,tmp);
	int l=0,r=sb,ans=l;
	while(l<=r){
		int mid=(l+r)>>1;
		if(getrt(a,mid,tmp)!=getrt(b,mid,tmp)) l=mid+1;
		else ans=mid<<1,r=mid-1;
	}
	printf("YES\n%d\n",ans+sa-sb);
	return 0;
}

E. CF507E Breaking Good

在几乎摸到正解的时候看了题解,他妈的。

最短路是肯定的,考虑找到最优路径。显然我们找到的最短路好边越多越好,在题解区看到了一种简洁的证明方法:

受影响的道路等于最短路上的坏边数和不在最短路上的好边数。设最短路上有 x 条好边和 y 条坏边,一共有 s 条好边,则受影响的道路总数为 sx+y 条。又因为最短路上的坏边数等于 dx,其中 d 是最短路长度,所以最终受影响的边数就是 sx+dx=s+d2x 条。

s 固定,d 也固定,变量只有 x。那么显然 x 越大越好。

于是我们在最短路的时候以路径长度为第一关键字、好边数为第二关键字进行比较即可。需要用一个 pre 数组记录路径,跑完最短路时重新扫一下路径,如果最短路上有坏边记录答案,好边可以用一个 set 维护,遇到好边就从 set 里擦掉。

挺简单的。

G. CF125E MST Company

感觉如果用破圈的话这题的标签应该有 implementation,但貌似官方认可二分做法?

首先去掉 1 号点跑最小生成森林。设连出来的连通块个数为 x,则若 k<x,必然无解。

然后加入 1 号点向每个连通块连边权尽量小的边,这样就得到了一棵强制 x 度最小生成树。

然后不断地尝试把与 1 号点相连却没有进入最小生成树的边加入。每加入这样一条边,就需要删除由此所产生的环上的一条不与 1 相连的边。贪心地,我们应该删除边权最大的那一条。因为 n,k5000,所以找这条边可以 O(n) 算,跑一遍 DFS 就行。

重复上面的过程,直到最终得到一棵强制 k 度最小生成树。

代码实现的主要难点在于如何找到加边所产生的环。其实仔细想想不难,因为我们已经得到了一棵树,所以我们可以依次遍历 1 节点的子树,计算出 mx(u) 表示 u 到 1 的路径上边权最大的边。向一棵子树多连一条边后,环肯定出现在这条边所连向的那个点到 1 节点的路径上,所以我们就找到了它。

H. CF1707C DFS Trees

诈骗题。

题目的切入点首先应该是由边权各不相同推出 MST 唯一,处理出 MST 上的边。然后注意到这段伪代码:

if vis[v] = false
    add edge (u, v) into the set (s)
    dfs(v)

这三句的意思是,只要一个点没有走过,就会去走这个点,并把这条边加入最小生成树。这样的走法显然是盲目的,如果从一个点出发经过了不是 MST 上的边,那么这个伪代码求出的最小生成树就错了。

于是问题转化为,如何判定从一个点出发会不会走到非树边上。

容易发现的是,若一条非树边的两个端点为 u,v,设 dep(u)dep(v),若 LCA(u,v)v,则除了 uv 的子树以外的点都不合法,因为它们只能从上面走到 uv 中的一个,然后就会去走那条非树边。同理,若 LCA(u,v)=v,也就是说 vu 的祖先,那么树上从 vu 的链上的点为根的子树的点都不合法,因为这些点依然只能先走到 uv 中的一个,然后通过非树边走另外一个。

uv 本身是一定合法的,因为它们初始时就把自己走过了。

于是考虑给这些不合法的点加上一个权值,最后统计没有权值的点的个数。可以用树剖或树上差分解决。

I. CF1051F The Shortest Statement

初看题目怀疑自己,一看题解想骂自己,调完代码想笑自己。

突破口就是题目所给的 mn20。让我们充分动用人类智慧,想到如果把这个图搞成一棵生成树,那么多出来的非树边顶多 21 条。再仔细想想,就可以把原图的最短路分成两种情况:

  • 只经过树边的;
  • 经过至少一条非树边的。

前者直接在生成树上跑 LCA 就可以 O(logn) 地计算,后者只需以所有非树边的端点为原点跑 Dijkstra 就可以 O(42mlogn) 预处理 + O(42) 扫一遍查询。每一组询问对所有情况取 min 即可。这题做完了。


是不是觉得很简单,你可能十分钟就把生成树板子 + LCA 板子 + Dijkstra 板子码完了,但是这题有意思的地方才刚刚开始。下文仅是个人用 2.5h 调代码过程中的一些心得。

  • 首先,处理生成树的时候,必须把 m 条边全部扫完,而不是加够了 n1 条边就跳出。否则你会漏掉某些非树边并过不去第二个样例。

  • 其次,我用 vis 数组处理非树边端点,切记不要和 Dijkstra 的 vis 数组搞混,尽管没有样例能测出来这个错误。

  • 最后划重点,我把 42 个非树边的端点,每个点建立了一个结构体存储 dis 数组和 Dijkstra 函数,然后存到了一个 vector 里面。但我最开始在每组询问遍历这个 vector 的时候写的是:

    for(auto x:V)

    此时对奇技淫巧非常熟悉的同学可能就发现了华点:没错,这样写等于每次都要把这个结构体给 x 拷一遍,直接让你 TLE 到飞起。况且由于这样写等于在 main 函数内部开了一个 1e5 的数组,会直接 MLE!
    正确的写法是:

    for(auto& x:V)

    话说奇技淫巧好像是我写的

  • 最后,链式前向星在这道题就是比邻接表快。

下课。

posted @   Laoshan_PLUS  阅读(11)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示