当前位置: 首页 > news >正文

【超详细!题解|两种做法】洛谷P3196 [HNOI2008] 神奇的国度[MCS算法]

温馨提示:本题有大量引理,看完证明直接背结论即可。

前置知识:

团:图中的一个顶点子集,其中任意两个不同的顶点之间都有边相连,即该子集诱导出的子图是一个完全图

弦图:指图中每一个长度大于3的环(圈)都至少有一条弦的图,当然弦图也可以没有环。

           没有环的特例:树、路径。

弦:连接环上两个不相邻顶点的边。

完美消除序列:图中顶点的一个特定排列顺序,满足:

对于序列中的每个顶点 v,在序列中位于 v 之后的所有顶点中,v的邻点构成一个,即这些邻点之间两两相连

看不懂?我来举个例子。

这是一个弦图:

    A/ \B---C| /D---E

边集为:\left \{ {A-B, B-C, C-A, B-D, C-D, D-E} \right \}

我们来看看 E,D,C,B,A 是不是完美消除序列:

  1. 顶点 E(序列位置1):

    • 后续顶点:D, C, B, A
    • E 的邻点:只有 D
    • 验证:单个顶点 \left \{ D \right \} 自然形成团 ✓
    • 消除 E 后,图变为 A-B-C-D 组成的"四边形加对角线"
  2. 顶点 D(序列位置2):

    • 后续顶点:C, B, A
    • D 的邻点(在后续顶点中):C, B
    • 验证:C 和 B 之间有边,\left \{ {C,B} \right \} 形成团 
    • 消除 D 后,图变为:三角形 ABC
  3. 顶点 C(序列位置3):

    • 后续顶点:B, A
    • C 的邻点(在后续顶点中):B, A
    • 验证:B 和 A 之间有边,\left \{ {B,A} \right \} 形成团 
    • 消除  C 后,图变为:边 A-B
  4. 顶点 B(序列位置4):

    • 后续顶点:A
    • B 的邻点(在后续顶点中):A
    • 验证:单个顶点 \left \{ {A} \right \} 形成团 
    • 消除 B 后,图变为:单个顶点 A
  5. 顶点 A(序列位置5):

    • 没有后续顶点
    • 验证:条件满足 

得:E,D,C,B,A 是完美消除序列

同学们可以自己试一下,E, D, B, C, A 也是完美消除序列。

但是 B, A, C, D, E 不是,因为删除 B 后,后续节点 A, C, D, E 中的邻点 A, C, D 不成环。

其实完美消除序列就像拆房子,每次拆掉最外围的点,这样内部才能一直保持稳定(成团)

说回本题的前置知识:

弦图判定基础:一个无向图是弦图当且仅当它存在完美消除序列

证明:

必要性:如果一个图是弦图,那么它存在完美消除序列。

引理:弦图中至少存在一个单纯点(其邻点构成一个团的顶点)。

        反证引理:如果弦图不存在单纯点,如果这个弦图没有环,那么就是树或者路径,

                          从叶子节点和路径的开头或结尾往前遍历,就是完美消除序列

                          那假设这个图有环,考虑这个图包含度数最小点的最小环

                          最小环大小必定为 3,不然必须有弦,那就不是最小环,矛盾。

                          长度为 3 的环肯定有单纯点,因为去掉那个度数最小的点,剩下两个点连通

                          至此,弦图肯定存在单纯点。

充分性:如果一个图存在完美消除序列,那么它是弦图。

              同学们自己想下,证明不难,意会即可,这里不多赘述。

讲了一堆,现在回到本题:

题意:

给一个弦图,求它的最小染色数。

最小染色定义(最小点色数):为图中每个顶点分配一种颜色,使得相邻顶点颜色不同

(为啥是弦图?因为三角关系符合定义。

    那为啥是最小染色呢?因为要求相互连边的点不能组成一队,求最少队数,和最小染色一样)

解析:

求弦图的最小染色,实际上就是求弦图中的最大团的大小 n

因为团内节点都互相连边,颜色必须不同,所以最小染色数 \geq 最大团的大小 n

然而,该弦图中不可能还有更多颜色的需求,因为最多只有 n 个节点是互相相邻的

所以有最小染色数 = 最大团的大小 n

那如何求最大团的大小呢?

我们想到了完美消除序列的每个点与后续点中的邻点组成的

定理:在弦图的任何完美消除序列中,最大团必定是某个顶点与其后续邻点构成的团

证明:设最大团 C 第一个在完美消除序列中出现的点为 v

           N(v) 为 v 的邻点集合,B(v) 为 v 的后续节点的集合。

           那么就有:C\subseteq v\cup (N(v)\cap B(v))

           又因为 C 是最大团,所以 v\cup (N(v)\cap B(v)) 大小刚好和 C 相等。

很好,我们就只要求出一个完美消除序列就好。

先看代码,我待会再解释:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
template<typename T> void qread(T &x){x=0; int f=1; char c=getchar();for(; !isdigit(c); c=getchar()) if(c=='-') f=-1;for(; isdigit(c); c=getchar()) x=x*10+(c-'0');x*=f;
} 
int lab[N], p[N], n;
bool v[N], mp[N][N];
void solve(){memset(lab, 0, sizeof(lab));memset(v, 0, sizeof(v));for(int i=n; i>=1; i--){int t=0;for(int j=1; j<=n; j++){if(!v[j] && ((t==0) || (lab[j]>lab[t])))t=j;}v[t]=1; p[i]=t;for(int j=1; j<=n; j++){if((v[j]==0) && (mp[t][j]==1))lab[j]++;}}
}
int main(){int m; qread(n); qread(m);for(int i=1; i<=m; i++){int x, y; qread(x); qread(y);mp[x][y]=mp[y][x]=1;} solve();int ans=0;for(int i=1; i<=n; i++)ans=max(ans, lab[i]+1);printf("%d\n", ans);return 0;
}

别急,我知道你要问什么:

solve()
for(int i=n; i>=1; i--)

这里为什么要倒着遍历?

其实这段代码的算法叫MCS(最大势搜索),代码中的 lab 数组就是各个点的“势”

lab[i] 代表在当前求出的完美消除序列中,

在点 i 后面且是点 i 邻点的点的数量,也就是能和点 i 组成团的点数。

最后取答案的时候 lab 加 1,就是因为团的点数量不光包含后续邻点,还有点 i 自己

而 solve 函数是为了求出倒序的完美消除序列,每一步都找出当前图中最大势的点

最大势点是与已选的点连边最多的点,这样一步步选下来,

会发现(最后一个选的点)完美消除序列的最开始的点就是和所有点连边最少的点。

符合我们“拆房子要一开始先拆掉外围的点”。

那这样为什么能求出完美消除序列?

还有一开始 t 的选点,大家的 lab 都为 0,那岂不是随机选的吗?

先回答第一个问题,用反证法

假设对于点 x,我们倒序先选了它的邻点 y 和 z

也就是 y 和 z 在 x 的后续节点集合中,但却不相连边。

那么整张图的一部分一定长这个样子:

    z /   \
x     ...\   /y

为什么是一个环?因为如果没有环,就没理由 mcs 的时候先选 z 和 y 不选 x

发现了华点:长度大于等于 3 的环没有弦!不符合弦图的定义。

所以 mcs 求出的一定是完美消除序列。

再来说第二个问题,其实 mcs 一开始还真就是随机选的点

再来看回这张图:

    A/ \B---C| /D---E

我们发现无论是选点 B 这种不怎么单纯的点,

还是选点 E 这种清纯小白花作为最后一个点

都能求出完美消除序列。

(同学们自己试一下,这里我提供 E,D,C,A,B 和 A,C,B,D,E 作为参考)

那究竟为什么可以随便选点作为最后一个点呢?

其实这要说到弦图的定义:

弦图移除任意顶点后,剩余图仍是弦图。

反证:如果剩余图不是弦图,那就是有长度大于等于 4 的环

           而那个被移除的点肯定不在环上,不然环就断了。

           但我们不可能移除一个点只移除一条弦,所以剩余图是弦图。

所以无论去掉哪个点,弦图都还是弦图,肯定能求出完美消除序列。

也就是可以随便选点作为最后一个点,剩下点也还是弦图,根据归纳法,易证随便选没事。

(最后一个点可以随便选,但第一个点可不能!!拆房子只能从外拆,第一个点必须是单纯点

接下来上带注释的豪华版代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
template<typename T> void qread(T &x){x=0; int f=1; char c=getchar();for(; !isdigit(c); c=getchar()) if(c=='-') f=-1;for(; isdigit(c); c=getchar()) x=x*10+(c-'0');x*=f;
} 
int lab[N], p[N], n; //lab数组存点的势,p数组存完美消除序列 
bool v[N], mp[N][N]; //v标记点是否被选进序列,mp是邻接表 
void solve(){memset(lab, 0, sizeof(lab));memset(v, 0, sizeof(v));for(int i=n; i>=1; i--){int t=0;for(int j=1; j<=n; j++){if(!v[j] && ((t==0) || (lab[j]>lab[t]))) t=j;//选出没进过序列且势最大的点 }v[t]=1; p[i]=t;  //标记,进序列 for(int j=1; j<=n; j++){if((v[j]==0) && (mp[t][j]==1))lab[j]++; //邻点加势 }}
}
int main(){int m; qread(n); qread(m);for(int i=1; i<=m; i++){int x, y; qread(x); qread(y);mp[x][y]=mp[y][x]=1;} solve();int ans=0;for(int i=1; i<=n; i++)ans=max(ans, lab[i]+1); //求后续邻点组成的最大团,别忘了加上点 i printf("%d\n", ans);return 0;
}

还有另一种时间复杂度更小的做法,本质理论是一样的 mcs。

因为每次 lab 只加 1,数据范围也不大,可以考虑造个,不同的 lab 值放不同的格子。

建个双向链表,桶里格子的每个节点都有 pre 和 next(前仆和后继)。

多开 n 个节点代表每个格子的头节点,头节点不存东西,

只是每次查找的时候看看头节点的 next 有没有值,来判断这个格子里有没有节点

每次插入的时候将节点 x 插到格子的最前面

光这么说有点空,直接看代码反而更好理解。

线性时间复杂度的代码:

#include<bits/stdc++.h>
using namespace std;
template<typename T> void qread(T &x){x=0; int f=1; char c=getchar();for(; !isdigit(c); c=getchar()) if(c=='-') f=-1;for(; isdigit(c); c=getchar()) x=x*10+(c-'0');x*=f;
} 
const int N=1e4+10;
int pre[2*N], nxt[2*N];
bool v[N];
int p[N], lab[N];
vector<int> G[N];
void push(int x){pre[nxt[x]=nxt[N+lab[x]]]=x; // x的下一个为原先第一个节点,原先第一个的上一个为 x nxt[pre[x]=N+lab[x]]=x; //x 的上一个为头节点,头节点的下一个为 x 
}
void del(int x){pre[nxt[x]]=pre[x];nxt[pre[x]]=nxt[x];
}
int main(){int n, m; qread(n); qread(m);for(int i=1; i<=m; i++){int x, y; qread(x); qread(y);G[x].push_back(y);G[y].push_back(x);}for(int i=1; i<=n; i++) push(i);int now=0, ans=0; // now就是当前遍历到哪一个 lab值 memset(v, 0, sizeof(v));memset(lab, 0, sizeof(lab));for(int i=n; i>=1; i--, now++){while(nxt[N+now]==0) now--; //等于 0就是这个格子里没东西 int x=nxt[N+now];del(x);p[i]=x; v[x]=1; //存进完美消除数组里 int sum=0; //统计团大小 for(int y: G[x]){if(!v[y]){ // y没遍历过,因为是倒序,所以 y是 x的后续相邻节点 del(y);lab[y]++;push(y);}else ++sum;}ans=max(ans, sum+1); //求团大小别忘了 x本身 }printf("%d\n", ans);return 0;
}

http://www.xdnf.cn/news/1289701.html

相关文章:

  • 深入剖析 React 合成事件:透过 onClick 看本质
  • 过程设计工具深度解析-软件工程之详细设计(补充篇)
  • Nginx 高级配置
  • 【后端】Spring @Resource和@Autowired的用法和区别
  • 通用同步/异步收发器USART串口
  • excel-随笔记
  • [ 数据结构 ] 时间和空间复杂度
  • Python初学者笔记第二十二期 -- (JSON数据解析)
  • VGG改进(2):基于Local Attention的模型优化
  • 【图像算法 - 13】基于 YOLO12 与 OpenCV 的实时目标点击跟踪系统(系统介绍 + 源码详细)
  • 获取数组,字符串,集合的长度
  • C++——高性能组件
  • 算法打卡力扣第88题:合并两个有序数组(easy)
  • 解释 Spring MVC 的工作原理
  • _init__.py的作用
  • 智能装配线cad【8张】三维图+设计说明书
  • linux 执行ls命令文件夹显示全白色
  • Langchain入门:文本摘要
  • 多轮问答与指代消解
  • 一维数组的创建、初始化与使用指南
  • “生成式UI革命”:Tambo AI如何让你的应用“开口说话、动手搭界面” | 全面深剖、案例实践与未来展望
  • Python函数篇:从零到精通
  • ubuntu24下keychorn键盘连接不了的改建页面的问题修复
  • 每日任务day0812:小小勇者成长记之挤牛奶
  • 10-docker基于dockerfile自动制作镜像
  • 熟悉并使用Spring框架 - 注解篇
  • golang的继承
  • 【Python办公】Mermaid代码转图片工具 - Tkinter GUI版本
  • NY198NY203美光固态闪存NY215NY216
  • Android 项目:画图白板APP开发(一)——曲线优化、颜色、粗细、透明度