当前位置: 首页 > 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.dtcms.com/a/328462.html

相关文章:

  • linux 开机进入initramfs无法开机
  • 嵌入式技术公开课精华笔记:CSDN专版
  • 【数据分享】1:25万水系线路分布数据(辽宁省)
  • Java 课程,每天解读一个简单Java之利用条件运算符的嵌套来完成此题:学习成绩>=90分的同学用A表示,60-89分之间的用B表示, * 60分以下
  • imx6ull-驱动开发篇19——linux信号量实验
  • 深度优先搜索和广度优先搜索(重制版)
  • [git] 重配ssh key | 解决冲突
  • 【SpringBoot】MyBatis 动态 sql
  • python——day2
  • 力扣(LeetCode) ——移除链表元素(C语言)
  • NumPy 创建空数组并逐个添加元素的深度解析
  • java理解
  • 解决微前端子应用嵌入后样式被覆盖
  • 深度解析 AS32S601 芯片 CAN Bus Off 机制:从原理到应用的全流程指南
  • 浏览器CEFSharp+X86+win7 之 全球外贸电商平台订单管理(十)
  • 前后端分离项目中Spring MVC的请求执行流程
  • uni-app实战教程 从0到1开发 画图软件 (学会画图)
  • Ceph BlueStore存储引擎详解
  • 【数据结构】并查集:从入门到精通
  • 《Linux基础知识-1》
  • docker-compose搭建 redis 集群
  • 阿里巴巴开源多模态大模型-Qwen-VL系列论文精读(一)
  • VBS 时间函数
  • 基于 libwebsockets 库实现的 WebSocket 服务器类
  • Shader warning in ‘Universal Render Pipeline/Particles/Simple Lit‘
  • provide()函数和inject()函数
  • 【UEFI系列】Super IO
  • VUE+SPRINGBOOT从0-1打造前后端-前后台系统-语音评测
  • 嵌入式学习(day25)文件IO:open read/write close
  • VGG改进(2):基于Local Attention的模型优化