%%%神犇vanilla!!!AK虐全场!!!
%%%神犇vanilla!!!AK虐全场!!!
%%%神犇vanilla!!!AK虐全场!!!
%%%神犇vanilla:vanilla为了算pow(x,1/3),他竟然想出了可以先算sqrt(sqrt(x))=y,再算pow(y,4/3)的优秀算法!!!
一、讲课
1、强连通分量
(1)定义
有向图G中,若两个顶点u,v之间能够相互到达,则称u,v是强联通的
若有向图G的每两个顶点都强连通,则G是一个强连通图
有向图的极大强连通子图,称为强连通分量
(2)一些推导
1.强连通分量是针对有向图的
2.有向图G中的每个点都属于且只属于一个强连通分量
3.若A,B属于同一个强连通分量,B,C属于同一个强连通分量,则A,C也属于同一个强连通分量
(3)算法
Tarjan
主体过程:
从某个点开始,DFS整个图,每访问到一个新的节点就将其压入栈中
为每个点记录两个量dfn和low
其中dfn[x]是时间戳,代表x是第几个被访问到的点
low[x]代表x最多只经过一条非树边(虚线)能到达的点中,dfn的最小值
维护low值的方式:对于x的每个出点j
若j还没被搜到,则先搜索j ,然后用low[j]来更新low[x]
否则若j还在栈中,则直接用dfn[j]来更新low[x]
x的出边全部遍历之后,若dfn[x]==low[x],则从栈顶到x的这部分属于同一个强连通分量,把这段弹栈
模板
void tarjan(int x) {
dfn[x]=low[x]=++idx;
s[++top]=x;
in[x]=1;
for(int u=head[x]; u; u=edge[u].next) {
int t=edge[u].to;
if(!dfn[t]) {
tarjan(t);
low[x]=min(low[x],low[t]);
}
else if(in[t]){
low[x]=min(low[x],dfn[t]);
}
}
if(dfn[x]==low[x]) {
tot++;
while(s[top]!=x){
ans[tot].push_back(s[top]);
in[s[top]]=0,top--;
}
ans[tot].push_back(s[top]);
in[x]=0,top--;
}
}
Kosaraju
复杂度高,用处很少,在此不再详述
(4)例题
[Usaco2006 Jan]The Cow Prom
直接上Tarjan就好了,顺便维护每个强连通分量的大小
[HAOI2006]受欢迎的牛
先缩点,然后看出度为0的强连通分量是否只有一个
如果有,则答案为这个强连通分量的大小
否则没有能被所有点到达的点
#include<bits/stdc++.h>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
#define fed(i,j) for(int i=head[j];i;i=e[i].nxt)
#define rd1 inline int read(){int x=0,f=1;char ch=getchar();
#define rd2 for(;!idg(ch);ch=getchar())if(ch=='-')f=-1;
#define rd3 for(;idg(ch);ch=getchar())x=x*10+ch-'0';return x*f;}
#define fastread rd1 rd2 rd3
#define rd(x) x=read()
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
using namespace std;
fastread
int n,m,a,b,tot,head[10005],tot2,head2[10005];
struct edge{
int t,nxt;
}e[50005],e2[50005];
void add(int u,int v){
e[++tot]=(edge){v,head[u]};
head[u]=tot;
}
bool in[10005];
int dfn[10005],low[10005],ans[10005];
int fr[10005],idx,s[10005],top,num,tmp;
void tarjan(int x){
dfn[x]=low[x]=++idx;
in[x]=1;s[++top]=x;
fed(i,x){int v=e[i].t;
if(!dfn[v]){
tarjan(v);
low[x]=min(low[x],low[v]);
}
else if(in[v])low[x]=min(low[x],dfn[v]);
}
if(dfn[x]==low[x]){
num++;
while(s[top]!=x){
in[s[top]]=0;
ans[num]++;
fr[s[top]]=num;
top--;
}
in[s[top]]=0;
ans[num]++;
fr[x]=num;
top--;
}
}
void added(int u,int v){
e2[++tot2]=(edge){v,head2[u]};
head2[u]=tot2;
}
int main(){
n=read(),m=read();
rep(i,1,m){
a=read(),b=read();
add(a,b);
}
rep(i,1,n)if(!dfn[i])tarjan(i);
rep(i,1,n)fed(j,i)if(fr[e[j].t]!=fr[i])
added(fr[i],fr[e[j].t]);
rep(i,1,num)if(!head2[i]){
if(tmp){
tmp=0;
break;
}
else tmp=ans[i];
}
printf("%d", tmp);
return 0;
}
BZOJ2140: 稳定婚姻
设大写字母为男性,小写字母是女性
连边,B->b(男->女)的是夫妻关系,b->B(女->男)的是情人关系
所以我们按照这种方式来建图,若Aa在同一个强连通分量中,则这对婚姻是不稳定的
2、双连通分量
(1)定义
无向图G中,若去掉点x及其所有相邻的边之后,G的连通块个数增加了,则称x是G的一个割点
(割点求法:tarjan算法如果点x有一个出点的low值>=dfn[x],则x是一个割点)
无向图G中,若去掉边e之后,G的连通块个数增加了,则称e是G的一个桥
双连通分量:
对于一个无向图G,若其不存在桥,则称其是一个边双连通图
一个无向图G的极大边双连通子图成为G的边双连通分量
对于一个无向图G,若其不存在割点,则称其是一个点双连通图
一个无向图G的极大点双连通子图成为G的点双连通分量
边双连通分量:
A,B在同一个边双中当且仅当A,B之间有两条边不相交路径
因此,每个点属于且只属于一个边双连通分量
注意:边双连通分量缩点后会变成一棵树
边双求法:还是用Tarjan算法,几乎和求强连通分量的方式一样
(从DFS树的角度考虑,我们把树边当成从上到下的有向边,非树边当成从下到上的有向边,这个图中的强连通分量就是正常无向图中的边双连通分量)
点双连通分量:
由于一个割点可能在多个点双连通分量里,所以需要在普通的tarjan算法基础上做一些改动,把边压入栈中而不是点,这样我们开始弹栈的时候最后剩下的这条边就会把割点留下了
(2)例题
[Usaco2006 Jan] Redundant Paths :
先把边双连通分量缩起来然后贪心地把所有度数为1的点一对一对的连起来
所以答案=⌈缩点之后度数为1的点的个数/2⌉
//复制的代码
#include<cstdio>
#include<string>
#include<cstring>
using namespace std;
int n;
struct map {
int s,t,x;
int next;
} a[150001];
int edge;
int head[10001];
int scc,cnt;
bool v[10001];
int s[10001],top;
int dfn[10001],low[10001],belong[10001];
int fa[10001];
int indeg[10001];
int tot=0;
inline void add(int s,int t) {
a[edge].next=head[s];
head[s]=edge;
a[edge].s=s;
a[edge].t=t;
a[edge].x=tot;
}
inline int min(int x,int y) {
if(x<y)
return x;
return y;
}
void tarjan(int d) {
int i,x;
cnt++;
dfn[d]=cnt;
low[d]=cnt;
top++;
s[top]=d;
v[d]=true;
for(i=head[d]; i!=0; i=a[i].next) {
x=a[i].t;
if(a[i].x==fa[d])
continue;
if(dfn[x]==0) {
fa[x]=a[i].x;
tarjan(x);
low[d]=min(low[d],low[x]);
} else if(v[x]&&low[d]>dfn[x])
low[d]=dfn[x];
}
if(dfn[d]==low[d]) {
scc++;
x=s[top];
top--;
while(x!=d) {
v[x]=false;
belong[x]=scc;
x=s[top];
top--;
}
v[x]=false;
belong[x]=scc;
}
}
inline void count_edge() {
int i;
for(i=1; i<=edge; i=i+2) {
if(belong[a[i].s]==belong[a[i].t])
continue;
indeg[belong[a[i].s]]++;
indeg[belong[a[i].t]]++;
}
}
int main() {
int n,m;
scanf("%d%d",&n,&m);
int s,t;
int i,j;
for(i=1; i<=m; i++) {
scanf("%d%d",&s,&t);
edge++;
tot++;
add(s,t);
edge++;
add(t,s);
}
memset(v,false,sizeof(v));
for(i=1; i<=n; i++)
if(belong[i]==0)
tarjan(i);
count_edge();
int leaf=0;
for(i=1; i<=scc; i++)
if(indeg[i]==1)
leaf++;
int ans=(leaf+1)/2;
printf("%d\n",ans);
return 0;
}
3、二分图匹配
(1)定义
设G=(V,E)为一个无向图,若V存在两个子集A,B,使得A∪B=V,A∩B=∅,且保证对于∀(i,j)∈E,有i∈A&&j∈B或i∈B&&j∈A,则称G是一个二分图(二部图)简而言之就是如果能把G划分成两部分,使得每一部分内都不自己和自己连边(详情咨询”wj和zsy的连边之战“),G就是一个二分图
二分图的判定:从每个连通块开始DFS,尝试染色,每次向外扩展时分两种情况讨论:
1.没染过色,那就染成相反的颜色然后从他开始DFS
2.染过色了,检验其是否合法,不合法退出,合法跳过
匹配:对于二分图G的一个子图M=(V,E)若E的任意两条边都不依附于V中的同一个顶点,则称M是一个匹配
最大匹配:若M是G的所有匹配当中边数最多的一个匹配,则称M是一个最大匹配(二分图的最大匹配可能有多个)
完美匹配:如果一个匹配中图中的每个顶点都和匹配中的某条边关联,则称这个匹配是一个完美匹配
Hall定理:有二分图G=(V,E),其中V划分为A,B,C是A的子集,且|C|=k,存在一个匹配M使得C中的每一个点都存在于匹配中,当且仅当对于∀1<=i<=k,C中任意i个点都至少和B中i个点相连 (证明略)
(2)算法
匈牙利(求最大匹配)
主要思想是不断地寻找增广路来使得总匹配数+1
模板:
bool match(int u){
rep(i,1,n){
if(v[i]||!m[u][i])continue;
v[i]=1;
if(!matching[i]||match(matching[i])){
matching[i]=u;
return 1;
}
}
return 0;
}
void hungarian(){
rep(i,1,l){
memset(v,0,sizeof(v));
if(match(i))ans++;
}
return;
}
重要知识点: 二分图最小点覆盖 = 二分图最大匹配 二分图最大点独立集 = 总点数 - 二分图最大匹配
(3)例题
[Usaco2005 nov]Asteroids : 我们把对行和列建一个二分图,若存在小行星(i,j),就在行i和列j之间连一条边,现在问题就转化为了求二分图最小点覆盖,直接跑匈牙利算法即可
#include<cstdio>
using namespace std;
#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
struct map {
int s,t;
int next;
} a[1000001];
int head[100001];
int edge;
bool v[100001];
int lk[100001];
int n;
inline void add(int s,int t) {
a[edge].next=head[s];
head[s]=edge;
a[edge].s=s;
a[edge].t=t;
}
inline bool find(int d) {
int i;
for(i=head[d]; i!=0; i=a[i].next) {
int t=a[i].t;
if(!v[t]) {
v[t]=true;
if(lk[t]==0||find(lk[t])) {
lk[t]=d;
return true;
}
}
}
return false;
}
inline int work() {
int i;
int ans=0;
for(i=1; i<=n; i++) {
memset(v,false,sizeof(v));
if(find(i))
ans++;
}
return ans;
}
int main() {
int k;
scanf("%d%d",&n,&k);
int i;
int x,y;
for(i=1; i<=k; i++) {
scanf("%d%d",&x,&y);
edge++;
add(x,y+n);
}
printf("%d\n",work());
return 0;
}
泥泞的牧场(今天考试题): 建二分图,A部是所有横着的板子,B部是所有竖着的板子,然后对于所有泥坑,把他相关的两个板子连起来,这样选择一个点就代表选择了一块板子,最小点覆盖就相当于用最小的板子数来覆盖所有泥坑了
4、2—SAT
(1)奇怪的,我不懂的定义 SAT是Satisfiability(适应性)的缩写,SAT问题(适应性问题)指的是给出一些条件(元素经过逻辑运算的结果,如x1 andx2 or x3 xor x4 = a),问是否存在一种合法的方案使得其满足所有的条件。SAT问题已经被证明是一个NP完全问题
(https://baike.baidu.com/item/NP%E5%AE%8C%E5%85%A8%E9%97%AE%E9%A2%98/4934286?fr=aladdin)
2-SAT问题是一种特殊的SAT问题,它限制了每个表达式当中最多有两个未知量。2-SAT问题已经可以在多项式复杂度内求出100%正确解
(2)一些逻辑运算的转化: 为了解决2-SAT问题,我们可以把给出的二元关系式进行一些转换,变成形如“若A取值为x则B必须取值为y”这样的条件。
为了叙述方便,我们拿取值为0和1举例子
A&B=0 -> 若A取1则B必须取0,若B取1则A必须取0
A|B=1 ->若A取0则B必须取1,若B取0则A必须取1
A^B=1 -> …
对称性 对于特殊的限制条件,例如A=1(A&B=1),我们可以构建“若A取0则A必须取1”这样的限制条件
(3)建图: 对于每种元素,把它拆成两个点a[0],a[1]分别代表两种取值,那么对于条件“若A取x则B必须取y“,则从A[x]向B[y]连一条边。那么在图中,如果P能到Q,则代表“若P成立,则Q一定成立”。
那么如果a[0]连回到了a[1]或a[1]连到了a[0],则矛盾。(用Tarjan判断) 如果没有这种情况,就一定有解。 (证明略)
(4)判定某个元素的取值是否确定: 若a[0]可选,则加入边(a[1],a[0])后,依然合法。 若a[1]可选,则加入边(a[0],a[1])后,依然合法。 若两者皆合法,则取值不确定
(5)另一种求可行解的方法: 从1到n枚举元素,枚举取值,若取值合法,则继续,否则转到另外一种取值。 时间复杂度O(nm),不过可以用来求字典序最小/大的解。
例题:
[JSOI2010]满汉全席 直接放代码吧,bzoj
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<algorithm>
#include<cmath>
#include<cstring>
using namespace std;
int T,n,m;
int cnt,ind,scc,top;
int last[1005],dfn[1005],low[1005],q[1005],belong[1005];
bool inq[1005];
struct edge {
int to,next;
} e[20005];
void write() {
for(int i=1; i<=n; i++)
if(belong[2*i]==belong[2*i-1]) {
puts("BAD");
return;
}
puts("GOOD");
}
inline int read()
{
int x=0,f=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
}
int get() {
int x;char c=getchar();
while(c!='m'&&c!='h')c=getchar();
if(c=='m')x=read()*2;
else x=read()*2-1;
return x;
}
void add(int u,int v) {
e[++cnt].to=v;
e[cnt].next=last[u];
last[u]=cnt;
}
void readd() {
int x=get(),y=get(),xp,yp;
if(x%2==0)xp=x--;
else xp=x++;
if(y%2==0)yp=y--;
else yp=y++;
add(xp,y);
add(yp,x);
}
void init() {
top=ind=cnt=scc=0;
scanf("%d%d", &n, &m);
for(int i=1; i<=2*n; i++)
last[i]=dfn[i]=0;
for(int i=1; i<=m; i++)readd();
}
//读入输出部分结束
//tarjan
void tarjan(int x) {
low[x]=dfn[x]=++ind;
inq[x]=1;
q[++top]=x;
for(int i=last[x]; i; i=e[i].next)
if(!dfn[e[i].to]) {
tarjan(e[i].to);
low[x]=min(low[x],low[e[i].to]);
} else if(inq[e[i].to])
low[x]=min(low[x],dfn[e[i].to]);
if(low[x]==dfn[x]) {
scc++;
int now=0;
while(now!=x) {
now=q[top--];
belong[now]=scc;
inq[now]=0;
}
}
}
int main() {
scanf("%d", &T);
while(T--) {
init();
for(int i=1; i<=2*n; i++)
if(!dfn[i])tarjan(i);
write();
}
return 0;
}
[Usaco2011 Jan]奶牛议会
还是挂代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cctype>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(ll i=a;i<=b;i++)
#define dep(i,a,b) for(ll i=a;i>=b;i--)
#define rd1 inline ll read(){ll x=0,f=1;char ch=getchar();
#define rd2 for(;!idg(ch);ch=getchar())if(ch=='-')f=-1;
#define rd3 for(;idg(ch);ch=getchar())x=x*10+ch-'0';return x*f;}
#define fastread rd1 rd2 rd3
#define rd(x) x=read()
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
#define ll unsigned long long
using namespace std;
fastread
int n,m,cnt;
int last[2005],ans[2005];
bool mark[2005];
char ch[3]= {'?','N','Y'};
struct edge {
int to,next;
} e[8005];
int get() {
int x=read();char c=getchar();
while(c!='Y'&&c!='N')c=getchar();
if(c=='Y')x=x*2-1;
else x=x*2;
return x;
}
void insert(int u,int v) {
e[++cnt].to=v;
e[cnt].next=last[u];
last[u]=cnt;
}
void dfs(int x) {
mark[x]=1;
for(int i=last[x]; i; i=e[i].next)
if(!mark[e[i].to])dfs(e[i].to);
}
bool check(int x) {
memset(mark,0,sizeof(mark));
dfs(x);
rep(i,1,n)if(mark[2*i]&&mark[2*i-1])return 0;
return 1;
}
int main() {
n=read(),m=read();
rep(i,1,m) {
int a=get(),b,c=get(),d;
if(a&1)b=a+1;else b=a-1;
if(c&1)d=c+1;else d=c-1;
insert(b,c),insert(d,a);
}
rep(i,1,n) {
bool p=check(2*i-1);
bool q=check(2*i);
if(!p&&!q)return puts("IMPOSSIBLE"),0;
else if(p&&q)ans[i]=0;
else if(!p)ans[i]=1;
else ans[i]=2;
}
rep(i,1,n)printf("%c",ch[ans[i]]);
return 0;
}
二、考试
1、str
大水题,双指针维护,O(n)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cctype>
#include<queue>
#include<vector>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
#define wxh cout<<"wxh"<<endl;
using namespace std;
char ch[2000005];
int a[2000005],l,r,ans,len,num[26];
ll init;
void add(int x) {
init|=(1<<x);
}
bool check() {
if(init==67108863)return 1;
return 0;
}
bool check2() {
if(num[a[l]]>1)return 1;
return 0;
}
int main() {
// freopen("test.txt","r",stdin);
fin("str");fout("str");
gets(ch);
len=strlen(ch);
if(len<26)return printf("QwQ\n"),0;
rep(i,0,len-1)a[i+1]=ch[i]-'A';
l=1,r=1;
num[a[r]]++,add(a[r]);
while(!check()&&r<=len)num[a[++r]]++,add(a[r]);
if(r>len)return printf("QwQ\n"),0;
while(check2())num[a[l++]]--;
num[a[--l]]++;
ans=r-l+1;
while(r<=len) {
while(check2())num[a[l++]]--;
ans=min(ans,r-l+1);
if(r==len)break;
while(!check2()&&r<=len)num[a[++r]]++;
}
printf("%d\n", ans);
return 0;
}
2、game
呜呜呜,二分写挂。。。
先用pow(x,1/3)求出n*m的三次根号,再判断是否是整数以及n和m是否整除它。若都满足则为Yes,否则为No
证明显然
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cctype>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(ll i=a;i<=b;i++)
#define dep(i,a,b) for(ll i=a;i>=b;i--)
#define rd1 inline ll read(){ll x=0,f=1;char ch=getchar();
#define rd2 for(;!idg(ch);ch=getchar())if(ch=='-')f=-1;
#define rd3 for(;idg(ch);ch=getchar())x=x*10+ch-'0';return x*f;}
#define fastread rd1 rd2 rd3
#define rd(x) x=read()
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
using namespace std;
fastread
ll n,m,a[1000],b[1000];
ll get(ll x){
int l=0,r=1e6;
for(ll mid;l+1<r;){
mid=(l+r)>>1;
if(mid*mid*mid<x)l=mid;
else r=mid;
}
return r;
}
int main(){
// freopen("test.txt","r",stdin);
fin("game");fout("game");
int T=read();
while(T--){
n=read(),m=read();
ll mx=n*m;
ll x=get(mx);
if(x*x*x!=mx||n%x||m%x)
printf("No\n");
else printf("Yes\n");
}
return 0;
}
3、cover
上午讲过的水题,不做叙述
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<cctype>
#include<queue>
#include<vector>
#define ll long long
#define idg isdigit
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define dep(i,a,b) for(int i=a;i>=b;i--)
#define fed(i,j) for(int i=head[j];i;i=e[i].nxt)
#define rd1 inline int read(){int x=0,f=1;char ch=getchar();
#define rd2 for(;!idg(ch);ch=getchar())if(ch=='-')f=-1;
#define rd3 for(;idg(ch);ch=getchar())x=x*10+ch-'0';return x*f;}
#define fastread rd1 rd2 rd3
#define rd(x) x=read()
#define fin(x) freopen(x".in","r",stdin)
#define fout(x) freopen(x".out","w",stdout)
using namespace std;
fastread
//最小点覆盖=最大匹配
int n,m,matching[100005],ans=0;
int xnum,ynum,a[55][55],b[55][55];
char s[55][55];bool use[100005];
struct edge{
int t,nxt;
}e[100005];
int head[100005],tot=1;
void add(int u,int v){
e[++tot]=(edge){v,head[u]};
head[u]=tot;
}
bool match(int x){
fed(i,x)if(!use[e[i].t]){
use[e[i].t]=1;
if(!matching[e[i].t]||match(matching[e[i].t])){
matching[e[i].t]=x;
return 1;
}
}
return 0;
}
int main(){
// freopen("test.txt","r",stdin);
fin("cover");fout("cover");
n=read(),m=read();
rep(i,1,n)scanf("%s", s[i]+1);
rep(i,1,n)rep(j,1,m)if(s[i][j]=='*'){
if(j>1&&s[i][j-1]=='*')a[i][j]=a[i][j-1];
else a[i][j]=++ynum;
if(i>1&&s[i-1][j]=='*')b[i][j]=b[i-1][j];
else b[i][j]=++xnum;
}
rep(i,1,n)rep(j,1,m)if(s[i][j]=='*')
add(a[i][j],b[i][j]);
rep(i,1,ynum){
memset(use,0,sizeof(use));
if(match(i))ans++;
}
printf("%d\n", ans);
return 0;
}