图论相关问题
A. P2662 牛场围栏
同余最短路的板题,会了就没什么。见这篇。
B. P4211 [LNOI2014] LCA
感觉离线处理的技巧得多加磨练。
这题的暴力显然就是 \(O(nm)\) 或 \(O(nm\log n)\),暴力枚举 \([l,r]\) 和 \(z\) 的 LCA 的 \(\text{dep}\) 值,计算即可。
那么正解要么就是一次求出多个 LCA,要么就是把 \(\text{dep}\) 进行某种神奇的转化。
前者不太可做,后者还是可以想想的。
考虑将 \(\text{dep}_x\) 转化为 \(x\) 到根节点的节点个数。于是查询 \(\text{dep}_{\operatorname{LCA}(u,v)}\) 就可以转化为:将 \(u\) 到根节点的路径上的点权值 \(+1\),然后查询 \(v\) 到根节点的路径上的权值和。那么对于多个点也是同理的。
区间加、区间查,不是树剖还能是什么?不过我们不能直接暴力区间加和区间查,那样复杂度还是 \(O(nm\log n)\) 的。发现我们是要将 \([l,r]\) 的节点全部区间加,进而想到差分,也就是把 \(l-1\) 和 \(r\) 单拎出来,一个删一个加。
但是这样貌似无法做到在线维护了,考虑离线。把一次询问拆成一个 \([0,l-1]\) 和一个 \([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\) 向两边跳
- \(x\) 或 \(z\) 向中间跳
前者可以无限跳,但后者只能跳有限次数!因为一旦 \(\text{dis}(x,y)=\text{dis}(y,z)\),那么 \(x\) 和 \(z\) 都不能再向中间跳了,否则不合法。
反之,由 \(\text{dis}(x,y)=\text{dis}(y,z)\) 这种状态可以导出无限多种状态,这不由得我们考虑将这个状态设为一棵树的根节点,那么别的状态都是它的子孙。为了方便,我们将这个状态定义为 “初始状态”。
试着推广我们这个构想,对于任何一种状态,都能导出无限的状态,设其中两种分别为 \(a,b\),那么从 \(a\) 变到 \(b\) 的最少步数是不是就是它们在这棵树上先变到它们的 LCA,再从 LCA 变到另外一种状态?
有没有一种茅厕顿开的感觉!
别急,我们一步一步走。现在我们已经成功地将其转化为一个树上问题,还有几个问题亟待解决。首先,思考如何判断两个点之间不可达。其实是容易的,我们只要找到两个状态的初始状态,如果它们的初始状态不同,那么他俩就是不可达的。
但是又引出另外一个问题:如何找初始状态?值域在 \(10^9\),显然是不能暴力搜的。不过我们可以试着想,就算这三个数是 \(1,2,10^9\) 这种变态,我们实际上也可以一步转移,因为把 \(x,y,z\) 这种状态翻转到 \(y,x,z\) 这种状态时,\(x,y\) 两点间距离不变,事实上从相对位置来看可以看作直接平移。所以刚才那个变态最终转移到 \(10^9-2,10^9-1,10^9\) 我们实际上一步就能算出是走了 \(\left\lfloor\dfrac{\text{dis}(y,z)-1}{\text{dis}(x,y)}\right\rfloor\) 步,\(-1\) 是为了防止最后出现两点重合的情况。至此我们找到初始状态的复杂度应该就是 \(\log\) 级别了。
到这里,我们应该就能很快地判断出两个状态可不可达。接下来的任务是找出两个可达的点的最小步数。
显然,我们应该找两点的 LCA。但是显然什么倍增树剖都没法用,注意到刚才我们找初始状态的那个过程是可以限制步数的,所以我们应该可以通过找每一个状态向上跳父亲的次数来找 LCA,因为找到 LCA 之后再往上跳必定是相同节点,具有单调性。所以二分步数即可。注意在此之前应当先将 \(a,b\) 中较深的一个跳到和另外一个同样深,然后再执行二分。最后的答案就是二分出的答案 \(\times2\) 再加上较深的那个点预先跳的步数。
捋一下思路:先找到 \(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\) 条好边,则受影响的道路总数为 \(s-x+y\) 条。又因为最短路上的坏边数等于 \(d-x\),其中 \(d\) 是最短路长度,所以最终受影响的边数就是 \(s-x+d-x=s+d-2x\) 条。
\(s\) 固定,\(d\) 也固定,变量只有 \(x\)。那么显然 \(x\) 越大越好。
于是我们在最短路的时候以路径长度为第一关键字、好边数为第二关键字进行比较即可。需要用一个 \(\text{pre}\) 数组记录路径,跑完最短路时重新扫一下路径,如果最短路上有坏边记录答案,好边可以用一个 set 维护,遇到好边就从 set 里擦掉。
挺简单的。
G. CF125E MST Company
感觉如果用破圈的话这题的标签应该有 \(\textbf{implementation}\),但貌似官方认可二分做法?
首先去掉 \(1\) 号点跑最小生成森林。设连出来的连通块个数为 \(x\),则若 \(k<x\),必然无解。
然后加入 \(1\) 号点向每个连通块连边权尽量小的边,这样就得到了一棵强制 \(x\) 度最小生成树。
然后不断地尝试把与 \(1\) 号点相连却没有进入最小生成树的边加入。每加入这样一条边,就需要删除由此所产生的环上的一条不与 \(1\) 相连的边。贪心地,我们应该删除边权最大的那一条。因为 \(n,k\le5000\),所以找这条边可以 \(O(n)\) 算,跑一遍 DFS 就行。
重复上面的过程,直到最终得到一棵强制 \(k\) 度最小生成树。
代码实现的主要难点在于如何找到加边所产生的环。其实仔细想想不难,因为我们已经得到了一棵树,所以我们可以依次遍历 \(1\) 节点的子树,计算出 \(\text{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\),设 \(\text{dep}(u)\ge\text{dep}(v)\),若 \(\operatorname{LCA}(u,v)\ne v\),则除了 \(u\) 和 \(v\) 的子树以外的点都不合法,因为它们只能从上面走到 \(u\) 和 \(v\) 中的一个,然后就会去走那条非树边。同理,若 \(\operatorname{LCA}(u,v)=v\),也就是说 \(v\) 是 \(u\) 的祖先,那么树上从 \(v\) 到 \(u\) 的链上的点为根的子树的点都不合法,因为这些点依然只能先走到 \(u\) 和 \(v\) 中的一个,然后通过非树边走另外一个。
\(u\) 和 \(v\) 本身是一定合法的,因为它们初始时就把自己走过了。
于是考虑给这些不合法的点加上一个权值,最后统计没有权值的点的个数。可以用树剖或树上差分解决。
I. CF1051F The Shortest Statement
初看题目怀疑自己,一看题解想骂自己,调完代码想笑自己。
突破口就是题目所给的 \(m-n\le20\)。让我们充分动用人类智慧,想到如果把这个图搞成一棵生成树,那么多出来的非树边顶多 21 条。再仔细想想,就可以把原图的最短路分成两种情况:
- 只经过树边的;
- 经过至少一条非树边的。
前者直接在生成树上跑 LCA 就可以 \(O(\log n)\) 地计算,后者只需以所有非树边的端点为原点跑 Dijkstra 就可以 \(O(42m\log m)\) 预处理 + \(O(42)\) 扫一遍查询。每一组询问对所有情况取 \(\min\) 即可。这题做完了。
是不是觉得很简单,你可能十分钟就把生成树板子 + LCA 板子 + Dijkstra 板子码完了,但是这题有意思的地方才刚刚开始。下文仅是个人用 2.5h 调代码过程中的一些心得。
-
首先,处理生成树的时候,必须把 \(\boldsymbol m\) 条边全部扫完,而不是加够了 \(\boldsymbol{n-1}\) 条边就跳出。否则你会漏掉某些非树边并过不去第二个样例。
-
其次,我用 \(\text{vis}\) 数组处理非树边端点,切记不要和 Dijkstra 的 \(\text{vis}\) 数组搞混,尽管没有样例能测出来这个错误。
-
最后划重点,我把 \(42\) 个非树边的端点,每个点建立了一个结构体存储 \(\text{dis}\) 数组和 \(\text{Dijkstra}\) 函数,然后存到了一个 \(\tt vector\) 里面。但我最开始在每组询问遍历这个 \(\tt vector\) 的时候写的是:
\[\texttt{for(auto x:V)} \]此时对奇技淫巧非常熟悉的同学可能就发现了华点:没错,这样写等于每次都要把这个结构体给 \(x\) 拷一遍,直接让你 TLE 到飞起。况且由于这样写等于在 main 函数内部开了一个 1e5 的数组,会直接 MLE!
\[\texttt{for(auto& x:V)} \]
正确的写法是:话说奇技淫巧好像是我写的 -
最后,链式前向星在这道题就是比邻接表快。
下课。
标签:图论,初始状态,int,text,问题,LCA,相关,非树边,节点 From: https://www.cnblogs.com/laoshan-plus/p/18686448