强连通分量与2-SAT
强连通分量与2-SAT问题
强连通分量
思路的话,因为环肯定是一个强连通分量,那么我们的思路就在于不断找到构成环的节点,于是可以把访问中的节点标记,再次访问则表明有环,然后为了不重复统计,我们选择了整个强连通分量里
板子
void tarjan(int u){
t.push(u);vis[u]=1;//t是栈
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
int v;
scc_cnt++;
do{
v=t.top();t.pop();
vis[v]=0;scc[scc_cnt].push_back(v);
c[v]=scc_cnt;siz[scc_cnt]++;
}while(u!=v);
}
}
DAG的必经点必经边
对于一个
由于路径条数非常大,一般会将其取模
2-SAT
2-SAT问题是这样的,给定
当有解的情况,考虑如何给出一组合法的解
显然,同一个强连通分量的取值应该是绑在一起的,并且一个强连通分量会影响它所链接的所有强连通分量,考虑零出度的强连通分量不会影响任何强连通分量,那么可以一步步的按照拓扑排序来不断找零出度的强连通分量更新即可,那么我们在Tarjan算法中求出的强连通分量本就满足拓扑序,编号小的肯定先确定,所以直接比较两种取值所在强连通分量的编号大小即可
模版代码:
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
t.push(u);vis[u]=1;
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
int v;
cnt++;
do{
v=t.top();t.pop();
vis[v]=0;
c[v]=cnt;
}while(u!=v);
}
}
//在main函数中
for(int i=1;i<=n<<1;i++)if(!dfn(i))tarjan(i);
for(int i=1;i<=n;i++){
if(c[i]==c[s[i]]){
puts("无解");
return 0;
}
}
for(int i=1;i<=n;i++){
val[i]=c[i]>c[opp[i]];//取i+n*val[i]
}
题目
杀人游戏
[中山市选]杀人游戏
题目描述一位冷血的杀手潜入Na-wiat,并假装成平民。警察希望能在
问:根据最优的情况,保证警察自身安全并知道谁是杀手的概率最大是多少?
输入格式
第一行有两个整数
接下来有
注:原文zz敏感内容已替换
输出格式
仅包含一行一个实数,保留小数点后面
样例 #1
样例输入 #1
5 4
1 2
1 3
1 4
1 5
样例输出 #1
0.800000
提示
警察只需要查证
对于
题意简述:给定一张有向图上有
如何想到图论建模呢,事实上,当我们面对类似于这种单向的二元关系问题,并且需要借助关系确定某些信息时,便可以想图论建模的思路
那么梳理一下思路,如果我们将每个人认识的人进行连一条有向边,记作
很明显,如果若干个点处于同一个
引理
一般情况下,确定每一个元素的颜色,最少的操作次数一定为零入度点的个数,
先证必要性:
显然,零入度点不可能被其他点所确定,故至少需要零入度点个数的操作次数才能覆盖整张图
再证充分性:
一个点
然后我们来思考有无特殊情况,考虑原图中的一个点
结合引理,我们得到了本题的算法流程
- 建图,执行缩点
- 对于缩点后的
,统计零入度点数量,记为 ,统计是零入度点,所在强连通分量大小为1并且满足其所连接的强连通分量的入度均大于1的 数量,记为 - 最终答案为:
对于本题需要注意的点是,在缩点建立新图的时候,很有可能出现重边的情况,大部分使用
注意我们的
- 清空
,这一步可以开一个 记录上一个强连通分量所连接的强连通分量进行撤销,保证复杂度 - 遍历第
个强连通分量的元素 ,对 执行操作3 - 遍历
的所有出边,在同一个强连通分量的不管,不在同一个强连通分量的,设后继点为 ,则若 未曾标记,将其入度加一并标记,若被标记,则不管
时间复杂度
本题总时间复杂度:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>
#include<stack>
using namespace std;
#define N 150000
#define M 650000
int head[N],n,m,ans,p,num,ver[M],nxt[M],c[N],siz[N],in[N],cnt[N],tot,scc_cnt,vis[N],dfn[N],low[N],s[N],inn[N];
int shead[N],sver[M],snxt[M],stot;
void add_s(int u,int v){
snxt[++stot]=shead[u],sver[shead[u]=stot]=v;
}
vector<int>scc[N];
stack<int>t;
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
void tarjan(int u){
t.push(u);vis[u]=1;
dfn[u]=low[u]=++num;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])low[u]=min(low[u],dfn[v]);
}
if(dfn[u]==low[u]){
int v;
scc_cnt++;
do{
v=t.top();t.pop();
vis[v]=0;scc[scc_cnt].push_back(v);
c[v]=scc_cnt;siz[scc_cnt]++;
}while(u!=v);
}
}
bool check(int a){
for(int i=0;i<scc[a].size();i++){
int u=scc[a][i];
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(c[u]==c[v])continue;
if(in[c[v]]<2)return true;
}
}
return false;
}
void solve(){
if(n==1){
printf("1.000000\n");
return ;
};
for(int i=1;i<=scc_cnt;i++){
int num=0;
cnt[i]=1;
for(int j=0;j<siz[i];j++){
int u=scc[i][j];
for(int k=head[u];k;k=nxt[k]){
if(cnt[c[ver[k]]])continue;
in[c[ver[k]]]++;
cnt[c[ver[k]]]=1;
s[++num]=c[ver[k]];
}
}
cnt[i]=0;
while(num)cnt[s[num--]]=0;
}
int ans1=0,flag=1;
for(int i=1;i<=scc_cnt;i++){
if(in[i]==0){
ans1++;
}
}
if(ans1==1){
printf("%.6f\n",1.0-1.0/n);
return ;
}
for(int i=1;i<=scc_cnt;i++)if(in[i]==0&&!check(i)){ans1--;break;}
printf("%.6f\n",1.0*(n-ans1)/n);
return ;
}
void init(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int u,v;
scanf("%d%d",&u,&v);
add(u,v);
inn[v]++;
}
for(int i=1;i<=n;i++)if(!dfn[i])tarjan(i);
solve();
}
int main(){
init();
}
例题:银河
银河中的恒星浩如烟海,但是我们只关注那些最亮的恒星。
我们用一个正整数来表示恒星的亮度,数值越大则恒星就越亮,恒星的亮度最暗是 1。
现在对于 N 颗我们关注的恒星,有 M 对亮度之间的相对关系已经判明。
你的任务就是求出这 N 颗恒星的亮度值总和至少有多大。
输入格式
第一行给出两个整数 N 和 M。
之后 M 行,每行三个整数 T,A,B,表示一对恒星 (A,B) 之间的亮度关系。恒星的编号从 1 开始。
如果 T=1,说明 A 和 B 亮度相等。
如果 T=2,说明 A 的亮度小于 B 的亮度。
如果 T=3,说明 A 的亮度不小于 B 的亮度。
如果 T=4,说明 A 的亮度大于 B 的亮度。
如果 T=5,说明 A 的亮度不大于 B 的亮度。
思路:这是一个很明显的差不大于1的差分约束系统,考虑借助这个最优性质进行优化。不妨思考,因为只有大/等于的会向小的连边,并且构成了一个强连通分量,即代表这些点取值相同,那么题目就变为了求强连通分量,然后缩点差分约束。,因为缩点后是DAG,所以直接可以从0出度点开始一步步往回拓扑排序
#include<iostream>
#include<cstdio>
#include<stack>
#include<vector>
#include<cstring>
using namespace std;
#define N 1005000
#define M 6005000
#define int long long
int tot,cnt,head[N],nxt[M],ver[M],n,m,dfn[N],num,low[N],c[N],in[N],out[N],vis[N],cost[M],siz[N],d[N],ans;
int shead[N],snxt[M],sver[M],stot,scost[M];
stack<int>t;
void add(int u,int v,int w){
nxt[++tot]=head[u],ver[head[u]=tot]=v,cost[tot]=w;
}
void add_s(int u,int v,int w){
snxt[++stot]=shead[u],sver[shead[u]=stot]=v,scost[stot]=w;
}
void tarjan(int u){
dfn[u]=low[u]=++tot;
t.push(u);
vis[u]=1;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(!dfn[v]){
tarjan(v);
low[u]=min(low[u],low[v]);
}
else if(vis[v])low[u]=min(low[u],dfn[v]);
}
if(low[u]==dfn[u]){
int v;cnt++;
do{
v=t.top();t.pop();
c[v]=cnt;vis[v]=0;siz[cnt]++;
}while(v!=u);
}
}
bool init(){
scanf("%lld%lld",&n,&m);
for(int i=1;i<=m;i++){
int u,v,w;
scanf("%lld%lld%lld",&w,&v,&u);
if(w==1)add(u,v,0),add(v,u,0);
if(w==2)add(v,u,1);
if(w==3)add(u,v,0);
if(w==4)add(u,v,1);
if(w==5)add(v,u,0);
}
n++;
for(int i=1;i<n;i++){
add(n,i,1);
}
tarjan(n);
for(int u=1;u<=n;u++){
for(int j=head[u];j;j=nxt[j]){
int v=ver[j];
if(c[v]==c[u]){
if(cost[j]>0)return false;
}
else {
add_s(c[u],c[v],cost[j]);
}
}
}
d[c[n]]=0;
for(int u=cnt;u>0;--u){
for(int i=shead[u];i;i=snxt[i]){
int v=sver[i];
d[v]=max(d[v],d[u]+scost[i]);
}
}
for(int i=1;i<=cnt;i++)ans+=d[i]*siz[i];
printf("%lld",ans);
return true;
}
signed main(){
if(!init())printf("-1");
return 0;
}
例题3.北大ACM队的远足
给定一张 N 个点 M 条边的有向无环图,点的编号从 0 到 N−1,每条边都有一个长度。
给定一个起点 S 和一个终点 T。
若从 S 到 T 的每条路径都经过某条边,则称这条边是有向图的必经边或桥。
北大 ACM 队要从 S 点到 T 点。
他们在路上可以搭乘两次车。
每次可以从任意位置(甚至是一条边上的任意位置)上车,从任意位置下车,但连续乘坐的长度不能超过 q 米。
除去这两次乘车外,剩下的路段步行。
定义从 S 到 T 的路径的危险程度等于步行经过的桥上路段的长度之和。
求从 S 到 T 的最小危险程度是多少。
首先求
但因为
引理1:任意两条最短路经过的桥的顺序相同
证明:反证法,如果不相同,我们假设最短路1是从
引理2:任意两条最短路经过的两个桥中间相隔的距离相同
证明:反证法,假设最短路1经过的距离为
由此,我们任选一条最短路即可,至于
现在我们将最短路抽离出来单独放进数组里面,并且做一个路径长度前缀和,桥的长度前缀和,这时候这条最短路就成为了一条线段,其上有若干个点
那么我们回到解决这个问题:求用两条长度为
这个扫描,因为两条不太好处理,我们不妨考虑如果只有一条的情况。设
考虑转移:这里的
若这条边不是必经边,直接从
若这条边是必经边,那么我们令这条覆盖线段的右端点放在
最后我们还需要考虑两个线段连在一起的情况,类比上面以一条长度为
#include<bits/stdc++.h>
using namespace std;
const int N = 100005, M = 200005, mod = 1000000007;
int ver[M*2], edge[M*2], nxt[M*2], head[N], tot;
int f[2][N], deg[2][N], d[N], pre[N], n, m, s, t, bus;
bool bridge[M*2];
int a[N], b[N], cnt; // 长度、是不是桥
int sum[N], sum_bri[N], ds[N], dt[N], ds_min[N];
int occur[N], first_occur[N];
queue<int> q;
void add(int x, int y, int z) {
ver[++tot] = y, edge[tot] = z, nxt[tot] = head[x], head[x] = tot;
}
void topsort(int s, int bit) {
if (bit == 0) { // 只有正图需要求最短路
memset(d, 0x3f, sizeof(d));
d[s] = 0;
}
f[bit][s] = 1;
for (int i = 1; i <= n; i++)
if (deg[bit][i] == 0) q.push(i);
while (!q.empty()) {
int x = q.front(); q.pop();
for (int i = head[x]; i; i = nxt[i])
if ((i & 1) == bit) {
int y = ver[i];
f[bit][y] = (f[bit][y] + f[bit][x]) % mod; // 路径条数
if (bit == 0 && d[y] > d[x] + edge[i]) { // 最短路
d[y] = d[x] + edge[i];
pre[y] = i;
}
if (--deg[bit][y] == 0) q.push(y);
}
}
}
int main() {
int C; cin >> C;
while (C--) {
memset(head, 0, sizeof(head));
memset(deg, 0, sizeof(deg));
memset(f, 0, sizeof(f));
memset(bridge, 0, sizeof(bridge));
memset(occur, 0, sizeof(occur));
tot = 1; cnt = 0;
cin >> n >> m >> s >> t >> bus;
s++; t++;
for (int i = 1; i <= m; i++) {
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
x++, y++;
add(x, y, z); // 偶数边是正边(邻接表2, 4, 6,...位置)
add(y, x, z); // 奇数边是反边
deg[0][y]++; // 入度
deg[1][x]++; // 出度
}
topsort(s, 0);
if (f[0][t] == 0) { puts("-1"); continue; }
topsort(t, 1);
for (int i = 2; i <= tot; i += 2) {
int x = ver[i ^ 1], y = ver[i];
if ((long long)f[0][x] * f[1][y] % mod == f[0][t]) {
bridge[i] = true;
}
}
// O(M)判重边,用map可能超时
for (int x = 1; x <= n; x++) {
for (int i = head[x]; i; i = nxt[i]) {
if (i & 1) continue; // 只考虑正边
int y = ver[i];
if (occur[y] == x) {
bridge[i] = false;
bridge[first_occur[y]] = false;
} else {
occur[y] = x;
first_occur[y] = i;
}
}
}
while (t != s) {
a[++cnt] = edge[pre[t]];
b[cnt] = bridge[pre[t]];
t = ver[pre[t] ^ 1];
}
// reverse(a + 1, a + cnt + 1); 不反过来也可以
// reverse(b + 1, b + cnt + 1);
for (int i = 1; i <= cnt; i++) {
sum[i] = sum[i - 1] + a[i]; // 以i这条边为结尾(包含i)的前缀总长度
sum_bri[i] = sum_bri[i - 1] + (b[i] ? a[i] : 0);
}
ds_min[0] = 1 << 30;
for (int i = 1, j = 0; i <= cnt; i++) { // 恰好在i这条边的结尾处下车,前面的最小危险程度:ds[i]
// 双指针扫描,让j+1~i这些边乘车,j这条边有可能部分乘车
while (sum[i] - sum[j] > bus) j++;
ds[i] = sum_bri[j];
if (j > 0 && b[j]) ds[i] -= min(a[j], bus - (sum[i] - sum[j]));
ds_min[i] = min(ds[i], ds_min[i - 1] + (b[i] ? a[i] : 0)); // i之前搭一次车:ds_min[i],即书上的"ds[i]"
}
for (int i = cnt, j = cnt + 1; i; i--) { // 恰好在i这条边的开头处上车,后面的最小危险程度:ds[i]
// 双指针扫描,让i~j-1这些边乘车,j这条边有可能部分乘车
while (sum[j - 1] - sum[i - 1] > bus) j--;
dt[i] = sum_bri[cnt] - sum_bri[j - 1];
if (j <= cnt && b[j]) dt[i] -= min(a[j], bus - (sum[j - 1] - sum[i - 1]));
}
// 两段乘车分开的情况
int ans = 1 << 30;
for (int i = 1; i <= cnt; i++)
ans = min(ans, dt[i] + ds_min[i - 1]);
// 两段乘车接在一起,2*bus覆盖一次的情况
for (int i = 1, j = 0; i <= cnt; i++) {
while (sum[i] - sum[j] > 2 * bus) j++;
int temp = sum_bri[j];
if (j > 0 && b[j]) temp -= min(a[j], 2 * bus - (sum[i] - sum[j]));
ans = min(ans, temp + sum_bri[cnt] - sum_bri[i]);
}
cout << ans << endl;
}
}
一些总结。
,变量为真。- 平面图欧拉定理
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!