2-sat
2-sat
定义
2−SAT2-SAT2−SAT简单的说就是给出nnn个集合,每个集合有两个元素,已知若干个(a,b)(a,b)(a,b),表示aaa与bbb矛盾,然后从每个集合选择一个元素,判断能否一共选nnn个两两不矛盾的元素。
更通俗的讲,有nnn个变量,每种变量只有两种取值(一般为0和1),还有mmm个关系表达式,一般为 x1∧x2=false x_1 \land x_2=false x1∧x2=false 询问能否给这nnn个变量找出一组赋值,使得mmm个关系全部满足
逻辑关系
a∨b⇔(¬a→b)∧(¬b→a) a \lor b \Leftrightarrow (\lnot a \rightarrow b) \land (\lnot b \rightarrow a) a∨b⇔(¬a→b)∧(¬b→a)
- a成立或b成立 等价于若a不成立则b必成立 或 若b不成立则a必成立
拆点建图
对于每个变量xxx,我们建立两个点x和¬xx和 \lnot xx和¬x分别表示xxx取 ture
和 false
, 在存储方式上,可以给第iii个变量标号为iii,其对应反值为i+ni+ni+n, 对于表达式a∨ba \lor ba∨b可以转化为(¬a→b)∧(¬b→a)(\lnot a \rightarrow b) \land (\lnot b \rightarrow a)(¬a→b)∧(¬b→a),按照箭头方向建边即可
tarjan缩点
- 跑完tarjan缩点后判断
- 若iii和i+ni+ni+n在同一个sccsccscc里,那么显然无解,因为$$不可能即取000又取111
- 否则iii和i+ni+ni+n可能在一条链上或无关,那么一定存在可行解
构造可行解
- 因为蕴含关系具有传递性,选择后者冲突更小,即选择拓扑序较大的节点更容易没有冲突,即若i→i+ni \rightarrow i+ni→i+n,则我们选择i+ni+ni+n,若 i+n→ii+n \rightarrow ii+n→i, 我们选择iii
- 由于tarjantarjantarjan得到的是逆拓扑序,所以sccsccscc编号越小的点其拓扑序越大,所以我们应该选择sccsccscc编号较小的变量所对应的文字语义,根据以上选择原则,若我们认为iii为真,i+ni+ni+n为假,则得到
- 若scc[i]<scc[i+n]scc[i]<scc[i+n]scc[i]<scc[i+n],则令xi=1x_i=1xi=1
- 若scc[i]>scc[i+n]scc[i]>scc[i+n]scc[i]>scc[i+n],则令xi=0x_i=0xi=0
- 复杂度$O(n+m) $
P4782 【模板】2-SAT
题目描述
有 nnn 个布尔变量 x1∼xnx_1\sim x_nx1∼xn,另有 mmm 个需要满足的条件,每个条件的形式都是 「xix_ixi 为 true
/ false
或 xjx_jxj 为 true
/ false
」。比如 「x1x_1x1 为真或 x3x_3x3 为假」、「x7x_7x7 为假或 x2x_2x2 为假」。
2-SAT 问题的目标是给每个变量赋值使得所有条件得到满足。
代码
const int N=2e6+10;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=m;i++){int a,x,b,y;cin>>a>>x>>b>>y;if(x && y){e[a+n].pb(b);e[b+n].pb(a);}if(x && !y){e[a+n].pb(b+n);e[b].pb(a);}if(!x && y){e[a].pb(b);e[b+n].pb(a+n);}if(!x && !y){e[a].pb(b+n);e[b].pb(a+n);}}for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=n;i++){if(scc[i]==scc[i+n]) {cout<<"IMPOSSIBLE"<<endl;return;}}cout<<"POSSIBLE"<<endl;for(int i=1;i<=n;i++) cout<<(scc[i]<scc[i+n])<<" \n"[i==n];
}
P5782和平委员会
题目描述
根据宪法,Byteland 民主共和国的公众和平委员会应该在国会中通过立法程序来创立。 不幸的是,由于某些党派代表之间的不和睦而使得这件事存在障碍。 此委员会必须满足下列条件:
- 每个党派都在委员会中恰有 111 个代表。
- 如果 222 个代表彼此厌恶,则他们不能都属于委员会。
每个党在议会中有 222 个代表。代表从 111 编号到 2n2n2n。 编号为 2i−12i-12i−1 和 2i2i2i 的代表属于第 iii 个党派。
任务:写一程序读入党派的数量和关系不友好的代表对,计算决定建立和平委员会是否可能,若行,则列出委员会的成员表。
第一行有两个非负整数 n,mn,mn,m。他们各自表示:党派的数量 nnn 和不友好的代表对 mmm。
接下来 mmm 行,每行为一对整数 a,ba,ba,b,表示代表 aaa 和 bbb 互相厌恶。
如果不能创立委员会,则输出信息 NIE
。
若能够成立,则输出包括 nnn 个从区间 111 到 2n2n2n 选出的整数,按升序写出,每行一个,这些数字为委员会中代表的编号。
如果委员会能以多种方法形成,程序可以只输出它们的某一个。
思路
不要无脑拆变量为真和假,逻辑关系的本质就是找到符合条件的点去建边,我们要满足题中的条件,两个互相厌恶的人不能在一起,那么我们就把一个人和另外一个人的搭档建边就可以了
代码
const int N=1e6+10;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;auto to=[&](int x){if(x%2) return x+1;else return x-1;};for(int i=1;i<=m;i++){int a,b;cin>>a>>b;e[a].pb(to(b));e[b].pb(to(a));}for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=2*n;i+=2) if(scc[i]==scc[i+1]) {cout<<"NIE"<<endl;return;}for(int i=1;i<=2*n;i+=2){if(scc[i]<scc[i+1]) cout<<i<<endl;else cout<<i+1<<endl;}}
P4171满汉全席
题目描述
和模板一样,不能同时不满足两种关系
第一行包含一个数字 KKK(1≤K≤501\le K \le 501≤K≤50),代表测试文件包含了 KKK 组数据。
每一组测试数据的第一行包含两个数字 nnn 跟 mmm(n≤100n≤100n≤100,m≤1000m≤1000m≤1000),代表有 nnn 种材料,mmm 位评审员。
为方便起见,舍弃做法的中文名称而给予编号,编号分别从 111 到 nnn。
接下来的 mmm 行,每行都代表对应的评审员所拥有的两个喜好,每个喜好由一个英文字母跟一个数字代表,如 m1m1m1 代表这个评审喜欢第 111 个材料透过满式料理做出来的菜,而 h2h2h2 代表这个评审员喜欢第 222 个材料透过汉式料理做出来的菜。
每组测试数据输出一行,如果不会发生没有人能通过考核的窘境,输出 GOOD
;否则输出 BAD
(均为大写字母)。
思路
板子板子
代码
const int N=310;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=2*n;i++){e[i].clear();low[i]=dfn[i]=0;scc[i]=siz[i]=0;stk[i]=instk[i]=0;}cnt=top=tot=0;for(int i=1;i<=m;i++)//汉1满0{char x,y;int a,b;cin>>x>>a>>y>>b;if(x=='m' && y=='m'){e[a].pb(b+n);e[b].pb(a+n);}if(x=='m' && y=='h'){e[a].pb(b);e[b+n].pb(a+n);}if(x=='h' && y=='m'){e[a+n].pb(b+n);e[b].pb(a);}if(x=='h' && y=='h'){e[a+n].pb(b);e[b+n].pb(a);}}for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=n;i++) if(scc[i]==scc[i+n]) {cout<<"BAD"<<endl;return;}cout<<"GOOD"<<endl;
}
P3007The Continental Cowngress G
题目描述
由于对农场主约翰的领导不满,奶牛们已经从农场中分离出来,并成立了第一个大陆奶牛议会。基于「每头奶牛都能得到她想要的东西」这一原则,她们决定采用以下投票系统:
出席的 MMM 头奶牛将对 NNN 项立法议案进行投票。每头奶牛对两个(不同的)议案 BiB_iBi 和 CiC_iCi 分别投下「赞成」或「反对」票(在输入文件中用 Y
或 N
表示)。这些投票分别称为 VBiVB_iVBi 和 VCiVC_iVCi。
最终,议案的通过与否必须满足每头奶牛至少有一个投票结果符合她的意愿。例如,如果 Bessie 对议案 111 投了「赞成」票,对议案 222 投了「反对」票,那么在任何有效的解决方案中,要么议案 111 通过,要么议案 222 被否决(或者两者都满足)。
给定每头奶牛的投票情况,你的任务是找出哪些议案将被通过,哪些议案将被否决,以符合上述规则。如果没有解决方案,请输出 IMPOSSIBLE
。如果至少有一个解决方案,那么对于每个议案,显示:
Y
如果在每个解决方案中该议案都通过
N
如果在每个解决方案中该议案都被否决
?
如果存在一些解决方案中该议案通过,而在另一些解决方案中该议案没有通过
考虑以下投票集(每头奶牛投两票):
编号 | 111 | 222 | 333 |
---|---|---|---|
奶牛 111 | 赞成 | 反对 | |
奶牛 222 | 反对 | 反对 | |
奶牛 333 | 赞成 | 赞成 | |
奶牛 444 | 赞成 | 赞成 |
由此,两个解决方案满足每头奶牛:
- 议案 111 通过(这满足了奶牛 111、333 和 444)
- 议案 222 被否决(这满足了奶牛 222)
- 议案 333 可以通过或被否决(这就是有两个解决方案的原因)
事实上,这些是仅有的两个解决方案,因此答案是 YN?
。
第 111 行:两个用空格分隔的整数:NNN 和 MMM
第 222 行到第 M+1M+1M+1 行:第 i+1i+1i+1 行描述奶牛 iii 的投票情况,包含四个用空格分隔的字段——一个整数,一个投票,另一个整数,和另一个投票:Bi,VBi,Ci,VCiB_i,VB_i,C_i,VC_iBi,VBi,Ci,VCi
思路
很好的一道题,关于选择YYY还是NNN我们只需要判断拓扑序大小即可,$ ?$ 的情况如何判断呢,用tarjantarjantarjan缩点的目的是判断两个互斥的变量是否在一个环内,即两者互相可达出现既取真又取假的无解情况,考虑有解的情况
1.i→i+ni \rightarrow i+ni→i+n此时i+ni+ni+n被iii约束,我们选择i+ni+ni+n
2.i+n→ii+n \rightarrow ii+n→i此时iii被i+ni+ni+n约束,我们选择iii
由此可知,若iii和i+ni+ni+n互不约束,两种取值选择哪种都是合法的
即若iii和i+ni+ni+n都不能到达彼此则为???
由于本题 n≤1000n \leq 1000n≤1000 支持O(n2)O(n^2)O(n2), 可以选择dfsdfsdfs处理可达性
代码
const int N=2010;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=m;i++){int a,b;char x,y;cin>>a>>x>>b>>y;if(x=='N' && y=='N'){e[a].pb(b+n);e[b].pb(a+n);}if(x=='N' && y=='Y'){e[a].pb(b);e[b+n].pb(a+n);}if(x=='Y' && y=='N'){e[a+n].pb(b+n);e[b].pb(a);}if(x=='Y' && y=='Y'){e[a+n].pb(b);e[b+n].pb(a);}}for(int i=1;i<=2*n;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=n;i++) if(scc[i]==scc[i+n]) {cout<<"IMPOSSIBLE"<<endl;return;}vector<int>vis(2*n+1);auto dfs=[&](auto &&dfs,int u)->void{vis[u]=1;for(auto ed:e[u]){if(!vis[ed]) dfs(dfs,ed);}return;};for(int i=1;i<=n;i++){int x,y;fill(all(vis),0);dfs(dfs,i);x=vis[i+n];fill(all(vis),0);dfs(dfs,i+n);y=vis[i];if(x && !y) cout<<'N';if(!x && y) cout<<'Y';if(!x && !y) cout<<'?'; }}
cf27D
题意
给定一个环,环上有一些边,你可以选择把每条边建在环内或者环外,问是否有使得不存在边相交的建图方式,有的话输出具体方案
思路
若有两条边相交,则我们肯定选择一条建在环内,一条建在环外,因此每条边有两种状态:建在环内或者环外,考虑2−sat2-sat2−sat求解
怎么判断两条边相交呢,假设存在[x1,y1],[x2,y2][x1,y1],[x2,y2][x1,y1],[x2,y2], 由于是在环上,那么若两边相交则一定有
-x1<x2<y1<y2x1 < x2 < y1 < y2x1<x2<y1<y2
-x2<x1<y2<y1x2 < x1 < y2 < y1x2<x1<y2<y1
n2n^2n2判断相交建图即可
代码
cconst int N=210;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;vector<int>x(m+1),y(m+1);for(int i=1;i<=m;i++){cin>>x[i]>>y[i];if(x[i]>y[i]) swap(x[i],y[i]);}for(int i=1;i<=m;i++)//1内0外{for(int j=i+1;j<=m;j++){if((x[i]<x[j] && x[j]<y[i] && y[i]<y[j]) || (x[j]<x[i] && x[i]<y[j] && y[j]<y[i])){e[i].pb(j+m);e[i+m].pb(j);e[j].pb(i+m);e[j+m].pb(i);}}}for(int i=1;i<=2*m;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=m;i++){if(scc[i]==scc[i+m]) {cout<<"Impossible"<<endl;return;}}for(int i=1;i<=m;i++){if(scc[i]<scc[i+m]) cout<<'i';else cout<<'o';}
}
P3209平面图判定
题目描述
若能将无向图 G=(V,E)G=(V, E)G=(V,E) 画在平面上使得任意两条无重合顶点的边不相交,则称 GGG 是平面图。判定一个图是否为平面图的问题是图论中的一个重要问题。现在假设你要判定的是一类特殊的图,图中存在一个包含所有顶点的环,即存在哈密顿回路。
输入文件的第一行是一个正整数 TTT,表示数据组数 (每组数据描述一个需要判定的图)。接下来从输入文件第二行开始有 TTT 组数据,每组数据的第一行是用空格隔开的两个正整数 NNN 和 MMM,分别表示对应图的顶点数和边数。紧接着的 MMM 行,每行是用空格隔开的两个正整数 uuu 和 vvv (1≤u,v≤N)\left(1\leq u,v\leq N\right)(1≤u,v≤N),表示对应图的一条边 (u,v)\left(u,v\right)(u,v), 输入的数据保证所有边仅出现一次。每组数据的最后一行是用空格隔开的 NNN 个正整数,从左到右表示对应图中的一个哈密顿回路:V1,V2,…,VNV_1,V_2,…,V_NV1,V2,…,VN,即对任意 i≠ji\not=ji=j 有 Vi≠VjV_i\not=V_jVi=Vj 且对任意 1≤i≤N−11\leq i\leq N-11≤i≤N−1 有 (Vi,Vi+1)∈E\left(V_i,V_{i+1}\right)\in E(Vi,Vi+1)∈E 及 (V1,VN)∈E\left(V_1,V_N\right)\in E(V1,VN)∈E。输入的数据保证 100%100\%100% 的数据满足 T≤100,3≤N≤200,M≤10000T\leq100,3\leq N\leq200,M\leq10000T≤100,3≤N≤200,M≤10000。
包含 TTT 行,若输入文件的第 iii 组数据所对应图是平面图,则在第 iii 行输出 YES\text{YES}YES,否则在第 iii 行输出 NO\text{NO}NO,注意均为大写字母
思路
和上一题的区别在于环的顺序不是1−n1-n1−n那么我们只需要按照哈密顿路径映射编号即可,问题在于 m≤1e4m \leq 1e4m≤1e4 ,O(m2)O(m^2)O(m2)的建图方式是无法接受的,这里引入平面图的性质:m≤n∗3−6m \leq n*3-6m≤n∗3−6 , 即最多600600600条边,对于不符合m>n∗3−6m > n*3-6m>n∗3−6的情况直接返回nonono即可
代码
const int N=1210;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;for(int i=1;i<=N;i++){e[i].clear();low[i]=dfn[i]=0;scc[i]=siz[i]=0;stk[i]=instk[i]=0;}tot=top=cnt=0;vector<int>x(m+1),y(m+1);for(int i=1;i<=m;i++) cin>>x[i]>>y[i];vector<int>id(n+1);for(int i=1;i<=n;i++){int x;cin>>x;id[x]=i;}if(m>3*n-6) {no;return;}for(int i=1;i<=m;i++){if(id[x[i]]>id[y[i]]) swap(x[i],y[i]);}for(int i=1;i<=m;i++){int x1=id[x[i]];int y1=id[y[i]];for(int j=i+1;j<=m;j++){int x2=id[x[j]];int y2=id[y[j]];if((x1<x2 && x2<y1 && y1<y2) ||(x2<x1 && x1<y2 && y2<y1)){e[i].pb(j+m);e[i+m].pb(j);e[j].pb(i+m);e[j+m].pb(i);}}}for(int i=1;i<=2*m;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=m;i++) if(scc[i]==scc[i+m]) {no;return;}yes;}
例 P6378Riddle
题意
nnn 个点 mmm 条边的无向图被分成 kkk 个部分。每个部分包含一些点。
请选择一些关键点,使得每个部分恰有一个关键点,且每条边至少有一个端点是关键点。
第一行三个整数 n,m,kn,m,kn,m,k。
接下来 mmm 行,每行两个整数 a,ba,ba,b,表示有一条 a,ba,ba,b 间的边。
接下来 kkk 行,每行第一个整数为 www,表示这个部分有 www 个点;接下来 www 个整数,为在这个部分中的点的编号。
若可能选出请输出 TAK
,否则输出 NIE
。
对于全部的测试点,保证 1≤k,w≤n≤1061\le k,w\le n\le 10^61≤k,w≤n≤106,∑w=n\sum w=n∑w=n,1≤a,b≤n1\le a,b\le n1≤a,b≤n,0≤m≤1060\le m\le 10^60≤m≤106。
思路
$ 2-sat +$前缀优化建图
每个点有两个状态选或不选,显然是2−sat2-sat2−sat,对于一条边至少选一个点的条件比较容易解决,关键在于每个点集最多选一个点的条件,若选了一个点的话就不能选其他点,即对于$ i \in [l,r],我们要对,我们要对,我们要对i和和和[l,i-1] , [i+1,r]$ 两个区间内的所有点都建边,时间复杂度是O(n2)O(n^2)O(n2)的无法接受,这时候就可以考虑前缀优化建图将时间复杂度降到$O(6n) $
建好图之后就是板子了
代码
const int N=4e6+10;int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;++siz[cnt];}while(y!=x);}
}void solve()
{int n,m,k;cin>>n>>m>>k;for(int i=1;i<=m;i++){int a,b;cin>>a>>b;e[a+n].pb(b);e[b+n].pb(a);}for(int i=1;i<=k;i++){int w;cin>>w;int p;//前一个点for(int j=1;j<=w;j++){int x;cin>>x;e[x].pb(x+2*n);//向下e[x+3*n].pb(x+n);if(j!=1){e[p+2*n].pb(x+2*n);//向左e[x+3*n].pb(p+3*n);//向右e[x].pb(p+3*n);//左下e[p+2*n].pb(x+n);//右下}p=x;}}for(int i=1;i<=4*n;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=n;i++) if(scc[i]==scc[i+n]) {cout<<"NIE"<<endl;return;}cout<<"TAK"<<endl;
}
cf587D
题意
给你一张 n 个点 m 条边的无向图,边有颜色和边权。你要从中删去一些边,满足:
-
任意两条删掉的边没有公共的顶点。
-
任意两条剩余的、颜色相同的边没有公共的顶点。
-
删去的边的边权最大值最小。
求这个最小值和方案
思路
首先最大值最小可以想到二分,我们可以二分边权最大值,对于大于midmidmid的边不选,小于midmidmid的边可以选,每条边有选和不选两种状态,考虑2−sat2-sat2−sat,
首先对于条件1和条件2,不难发现若一个点有两条以上相同颜色的边则一定无解,则可以将边分成以下333类:
- 对于条件1,若边x,yx , yx,y有相同的顶点,那么需要建边x1→y0x_1 \rightarrow y_0x1→y0代表选xxx则不能选$y $
- 对于条件2, 若边x,yx , yx,y有相同的顶点且颜色相同,那么需要建边x0→y1x_0 \rightarrow y_1x0→y1代表不选xxx则必须选yyy
- 边权大于midmidmid的,建边x→x+mx \rightarrow x+mx→x+m代表不可能选xxx
- 对于第111类边,其肯定是O(m2)O(m^2)O(m2)的,考虑前缀优化建图
- 对于第222类边,经过特判之后,每个顶点最多有两条相同颜色的边,因此复杂度为O(m)O(m)O(m)
- 但是需要注意我们是按顶点建边的,对于第111类和第222类边其应该是独立的,因此在前缀优化建图方式上我们选择动态开点,这样不会将两个独立的部分联系在一起
代码
const int N=2e6+10;
int dfn[N],low[N],tot;
int stk[N],instk[N],top;
int scc[N],siz[N],cnt;struct node{int v,c,w,id;bool operator < (const node&t)const{return c<t.c;}
};vector<node>e1[N];
vector<int>e[N];void tarjan(int x)
{dfn[x]=low[x]=++tot;stk[++top]=x;instk[x]=1;for(auto ed:e[x]){if(!dfn[ed]){tarjan(ed);low[x]=min(low[x],low[ed]);}else if(instk[ed]){low[x]=min(low[x],dfn[ed]);}}if(dfn[x]==low[x]){int y;++cnt;do{y=stk[top--];instk[y]=0;scc[y]=cnt;siz[cnt]++;}while(y!=x);}
}void solve()
{int n,m;cin>>n>>m;vector<int>w(m+1);for(int i=1;i<=m;i++){int a,b,c,d;cin>>a>>b>>c>>d;w[i]=d;e1[a].pb({b,c,d,i});e1[b].pb({a,c,d,i});}for(int i=1;i<=n;i++)//一个点上2条以上同色边{sort(all0(e1[i]));for(int j=2;j<e1[i].size();j++){if(e1[i][j-2].c==e1[i][j].c) {no;return;}}}auto check=[&](int x)->bool{for(int i=0;i<N;i++){e[i].clear();low[i]=dfn[i]=0;scc[i]=siz[i]=0;stk[i]=instk[i]=0;}tot=cnt=top=0;int k=2*m;for(int i=1;i<=n;i++){if(e1[i].empty()) continue;int pp0=0,pp1=0;for(int j=0;j<e1[i].size();j++){int x0=e1[i][j].id;int x1=x0+m;int p0=++k;int p1=++k;e[x0].pb(p0);//向下连e[p1].pb(x1);if(j!=0){e[pp0].pb(p0);//右e[p1].pb(pp1);//左e[pp0].pb(x1);//右下e[x0].pb(pp1);//左下}pp0=p0;pp1=p1;}for(int j=1;j<e1[i].size();j++)//颜色匹配{if(e1[i][j-1].c==e1[i][j].c){e[e1[i][j-1].id+m].pb(e1[i][j].id);e[e1[i][j].id+m].pb(e1[i][j-1].id);}}}for(int i=1;i<=m;i++) if(w[i]>x) e[i].pb(i+m);//代表不选for(int i=1;i<=2*m;i++) if(!dfn[i]) tarjan(i);for(int i=1;i<=m;i++) if(scc[i]==scc[i+m]) return false;return true;};int l=-1,r=1e9+1;while(l+1<r){int mid=(l+r)>>1;if(check(mid)) r=mid;else l=mid;} if(r==1e9+1) {no;return;}check(r);vector<int>ans;for(int i=1;i<=m;i++) if(scc[i]<scc[i+m]) ans.pb(i);yes;cout<<r<<" "<<ans.size()<<endl;for(auto x:ans) cout<<x<<" ";
}