1.负环
(1)虫洞
#include<bits/stdc++.h>
using namespace std;
const int N=510;
const int M=5210;
int n,m1,m2;
int h[N],e[M],w[M],ne[M],idx;
int dist[N];
int cnt[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool spfa()
{
memset(dist,0,sizeof(dist));
memset(st,0,sizeof(st));
memset(cnt,0,sizeof(cnt));
queue<int>q;
//起点不一定在1,所以所有点可以作为起点入队
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=1;
}
while(q.size())
{
int t=q.front();
q.pop();
st[t]=0;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j]=dist[t]+w[i];
//从t到j多了一条边w[t][j]
cnt[j]=cnt[t]+1;
if(cnt[j]>=n)return true;
if(!st[j])
{
st[j]=1;
q.push(j);
}
}
}
}
return 0;
}
int main()
{
int t;
cin>>t;
while(t--)
{
memset(h,-1,sizeof(h));
idx=0;
cin>>n>>m1>>m2;
//双向正边
while(m1--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
}
//单向负边
while(m2--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,-c);
}
//存在负环则是YES
if(spfa())cout<<"YES"<<endl;
else cout<<"NO"<<endl;
}
return 0;
}
(2)观光奶牛
#include<bits/stdc++.h>
using namespace std;
//L个点,P条有向边,每个点有个权值f[i],每条边都有一个权值t[i]
//求图中的一个环,使环上各点的权值之和除以环上各边的权值之和最大
//输出最大值
const int N=1000+10;
const int M=5000+10;
//1000*1000/(1*1000)最大答案-->1000
//可以对答案进行二分l=0,r=1000
//每二分一次mid,check(mid)->判断是否存在一个环使mid<=(fi之和)/(ti之和)
//即(mid*ti-fi)之和<=0(注意找最大答案就要mid<=ans)
//每次check,一条u指向v,边权为w的边权变为:w*mid-fu,只需检查这个图是否存在负环
int n,m,f[N];
int h[N],e[M],ne[M],w[M],idx;
double dist[N];
bool st[N];
int cnt[N];
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool check(double mid)
{
memset(dist,0,sizeof(dist));
memset(st,0,sizeof(st));
memset(cnt,0,sizeof(cnt));
queue<int>q;
for(int i=1;i<=n;i++)
{
q.push(i);
st[i]=1;
}
while(q.size())
{
int t=q.front();
q.pop();
st[t]=0;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
double ww=w[i]*mid-(double)f[t];
if(dist[j]>dist[t]+ww)
{
dist[j]=dist[t]+ww;
cnt[j]=cnt[t]+1;
if(cnt[j]>=n)return true;
if(!st[j])
{
st[j]=1;
q.push(j);
}
}
}
}
return false;
}
int main()
{
memset(h,-1,sizeof(h));
cin>>n>>m;
for(int i=1;i<=n;i++)cin>>f[i];
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
double l=0,r=1000,eps=1e-4;
//浮点数二分直接取二分取mid
while(r-l>eps)
{
double mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
printf("%.2lf\n",r);
return 0;
}
(3)单词环
#include<bits/stdc++.h>
using namespace std;
const int N=700,M=1e5+10;
int n;
int h[N],e[M],w[M],ne[M],idx;
double dist[N];
int cnt[N];
bool st[N];
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool check(double mid)
{
memset(st,0,sizeof(st));
memset(cnt,0,sizeof(cnt));
queue<int>q;
for(int i=0;i<676;i++)
{
q.push(i);
st[i]=1;
}
int count=0;
while(q.size())
{
int t=q.front();
q.pop();
st[t]=0;
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
if(dist[j]<dist[t]+w[i]-mid)
{
dist[j]=dist[t]+w[i]-mid;
cnt[j]=cnt[t]+1;
if(++count>10000)return true;
if(cnt[j]>=N)return true;
if(!st[j])
{
q.push(j);
st[j]=1;
}
}
}
}
return false;
}
int main()
{
char str[1010];
while(cin>>n)
{
if(n==0)break;
memset(h,-1,sizeof(h));
idx=0;
for(int i=1;i<=n;i++)
{
cin>>str;
int len=strlen(str);
if(len>=2)
{
int left=(str[0]-'a')*26+str[1]-'a';
int right=(str[len-2]-'a')*26+str[len-1]-'a';
add(left,right,len);
}
}
if(!check(0))puts("No solution");
else
{
double l=0,r=1000;
while(r-l>1e-4)
{
double mid=(l+r)/2;
if(check(mid))l=mid;
else r=mid;
}
printf("%lf\n",r);
}
}
return 0;
}
2.二分图
二分图(在一个连通块中):
(1)图中不存在奇数环(边的数目为奇数)
(2)染色法过程中不存在矛盾
(两个结论等价)
可以把所有的点划分成两边(一条边所连两个点颜色不同)
若某个连通块中一个点确定,即整个连通块都可以确定
(1)关押罪犯
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
const int M=2e5+10;
int h[N],e[M],ne[M],w[M],idx;
int n,m;
int cor[M];//0未染,再染1和2
//两个罪犯有怨气值就连边
void add(int a,int b,int c)
{
e[idx]=b;
w[idx]=c;
ne[idx]=h[a];
h[a]=idx++;
}
bool dfs(int x,int co,int mid)
{
cor[x]=co;
for(int i=h[x];~i;i=ne[i])
{
int j=e[i];
if(w[i]<=mid)continue;//在一个监狱内
if(cor[j])//已染但出现矛盾
{
if(cor[j]==co)return false;
}
else if(!dfs(j,3-co,mid))return false;//未染
}
return true;
}
bool check(int mid)
{
//每次check都重新染图
memset(cor,0,sizeof(cor));
for(int i=1;i<=n;i++)
{
if(!cor[i])
{
//未染过色
if(!dfs(i,1,mid))return false;//染1失败
}
}
return true;
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
add(b,a,c);
//二分图为无向图
}
//最大值最小化
int l=0,r=1e9;
while(l<r)
{
int mid=(l+r)/2;
if(check(mid))r=mid;
else l=mid+1;
}
cout<<r<<endl;
return 0;
}
(2)棋盘覆盖
匈牙利算法
二分图的最大匹配
最大匹配等价于不存在增广路径
每个卡片塞2个格子
把格子看成点
把卡片看成边
则只要能放卡片的相邻两个格子就连一条边
🔺问题转化为->最多取多少条边,满足卡片不重叠--所有选出的边没有公共点--二分图
放最多的牌--找到最多的边--二分图上最大匹配
图是不是二分图 <=> 能不能把图染成两种颜色 <=> 每个边上的两个点的颜色不同
=> 把棋盘按1间隔染色 => 二分图
每个格子横纵坐标之和分奇数偶数染
#include<bits/stdc++.h>
using namespace std;
#define x first
#define y second
typedef pair<int, int> PII;
const int N = 110;
int n, m;
PII match[N][N];
bool g[N][N], st[N][N];
int dx[4] = {-1, 0, 1, 0}, dy[4] = {0, 1, 0, -1};
bool find(int x, int y)
{
for (int i = 0; i < 4; i ++ )//枚举邻点
{
int a = x + dx[i], b = y + dy[i];
if (a && a <= n && b && b <= n && !g[a][b] && !st[a][b])//不是坏点 没遍历过
{
// 则男[x,y] 和 女[a,b]能够配对
st[a][b] = true;
PII t = match[a][b];//
//1 t.x==-1说明女[a,b]还没和其他人配对 则男[x,y]和女[a,b]可以直接配对
//2 女[a,b]已经有人配对,但和女[a,b]配对的男t还有其他选项
// 男t放弃和女[a,b]配对 让女[a,b]给男[x,y]配对
if (t.x == -1 || find(t.x, t.y))
{
match[a][b] = {x, y};
return true;
}
}
}
return false;
}
int main()
{
cin >> n >> m;
while(m--)
{
int x,y;
cin >> x >> y;
g[x][y] = true;
}
memset(match,-1,sizeof match);
int res = 0;
// 枚举所有和为奇数的点
for(int i=1;i<=n;i++)
{
for(int j = 1;j<=n;j++)
{
if((i+j)%2 && !g[i][j])
{
memset(st,0,sizeof st);//每次都需要清空st数组,因为匹配好的一对可能会有下家
if(find(i,j))res++;//如果[i,j]能配对
}
}
}
cout << res << endl;
return 0;
}
(3)机器任务
在二分图中:最小点覆盖==最大匹配数
最小点覆盖:能覆盖整个图所需要的点数(一边两个点,选一个点即是覆盖这条边)
#include<bits/stdc++.h>
using namespace std;
//某个任务为模式:a[i]==0||b[i]==0(直接完成不重启)
//剩下的任务a[i]>0&&b[i]>0
//(a+b)N+M-2个模式最少选择多少个模式,可以把所有任务做掉
//每个任务连一条边a[i]--b[i],最少选多少个点能覆盖所有的边(最小点覆盖问题=最大匹配数(匈牙利))
const int N=110;
bool g[N][N],st[N];
int n,m,k;
int match[N];
bool find(int x)
{
for(int i=1;i<=m-1;i++)
{
if(!st[i]&&g[x][i])
{
st[i]=1;
if(match[i]==-1||find(match[i]))
{
match[i]=x;
return true;
}
}
}
return false;
}
int main()
{
while(cin>>n,n)
{
cin>>m>>k;
memset(g,0,sizeof(g));
memset(match,-1,sizeof(match));
while(k--)
{
int t,a,b;
cin>>t>>a>>b;
if(!a||!b)continue;
g[a][b]=1;
}
int res=0;
//枚举a,匹配b
for(int i=1;i<=n-1;i++)
{
memset(st,0,sizeof(st));
if(find(i))res++;
}
cout<<res<<endl;
}
return 0;
}
3.拓扑序列
(1)家谱树
#include<bits/stdc++.h>
using namespace std;
//有向无环图(拓扑序列)
//输出一个序列,使得每个人的孩子都比那个人后列出。
const int N=400+10;
int h[N],e[N],ne[N],idx;
int n;
int d[N];
int ans[N],cnt;
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
void topsort()
{
queue<int>q;
for(int i=1;i<=n;i++)
{
if(!d[i])
{
q.push(i);
}
}
while(q.size())
{
int t=q.front();
ans[++cnt]=t;
q.pop();
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
d[j]--;
if(d[j]==0)q.push(j);
}
}
}
int main()
{
cin>>n;
memset(h,-1,sizeof(h));
for(int i=1;i<=n;i++)
{
int son;
while(cin>>son,son)
{
add(i,son);
d[son]++;
}
}
topsort();
for(int i=1;i<=cnt;i++)
{
cout<<ans[i]<<" ";
}
cout<<endl;
return 0;
}
(2)奖金
#include<bits/stdc++.h>
using namespace std;
const int N=2e4+10;
int n,m;
int h[N],e[N],ne[N],idx;
int d[N];
int ans[N],cnt;
int sal[N];
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
//入度为0的点为最低工资100元
void topsort()
{
queue<int>q;
for(int i=1;i<=n;i++)
{
if(!d[i])
{
q.push(i);
sal[i]=100;
}
}
while(q.size())
{
int t=q.front();
ans[++cnt]=t;
q.pop();
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
d[j]--;
if(!d[j])
{
sal[j]=sal[t]+1;
q.push(j);
}
}
}
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
while(m--)
{
int a,b;
cin>>a>>b;
//a的工资比b高,b->a
add(b,a);
d[a]++;
}
topsort();
if(cnt<n)puts("Poor Xed");
else
{
int res=0;
for(int i=1;i<=cnt;i++)
{
res+=sal[i];
}
cout<<res<<endl;
}
return 0;
}
(3)可达性统计(bitset)
#include<bits/stdc++.h>
using namespace std;
const int N=3e4 + 10;
bitset<N>f[N];//f[i]表示i能到达所有点的数量
//先拓扑排序,倒序求每个集合
//f[i]={i}Uf(j1)Uf(j2)....或运算
int h[N],e[N],ne[N],idx;
int d[N];
int n,m;
int ans[N],cnt;
void add(int a,int b)
{
e[idx]=b;
ne[idx]=h[a];
h[a]=idx++;
}
void topsort()
{
queue<int>q;
for(int i=1;i<=n;i++)
{
if(!d[i])q.push(i);
}
while(q.size())
{
int t=q.front();
ans[++cnt]=t;
q.pop();
for(int i=h[t];~i;i=ne[i])
{
int j=e[i];
d[j]--;
if(!d[j])q.push(j);
}
}
}
void cal()
{
//倒序枚举每一个
for(int j=cnt;j>=1;j--)
{
int x=ans[j];
f[x][x]=1;
for(int i=h[x];~i;i=ne[i])
{
int y=e[i];
f[x]|=f[y];//求出集合
}
}
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof(h));
while(m--)
{
int a,b;
cin>>a>>b;
add(a,b);
d[b]++;
}
topsort();
cal();
for(int i=1;i<=n;i++)
{
cout<<f[i].count()<<endl;//f[i].count()返回f[i]中1的个数
}
return 0;
}
(4)车站
#include<bits/stdc++.h>
using namespace std;
const int N = 2007, M = 5000007;
int n, m;
int ver[M], nex[M], edge[M], head[N], tot;
int din[N];
int vis[N];
int q[N];
int dist[N];
void add(int x, int y, int z){
ver[tot] = y;
edge[tot] = z;
nex[tot] = head[x];
head[x] = tot ++ ;
din[y] ++ ;
}
void toposort(){
int hh = 0, tt = -1;
for(int i = 1;i <= n + m;++i)
{//一共n + m个点,要遍历所有的点
if(!din[i]){
q[++ tt] = i;
if(tt == N)tt = 0;
}
}
while(hh <= tt){
int x = q[hh ++ ];
if(hh == N)hh = 0;
for(int i = head[x];~i;i = nex[i])
{
int y = ver[i];
if(-- din[y] == 0){
q[++ tt] = y;
if(tt == N)tt = 0;
}
}
}
}
int main(){
scanf("%d%d", &n,&m);
memset(head,-1,sizeof head);
for(int i = 1;i <= m;++i){
memset(vis,0,sizeof vis);
int t,stop;
scanf("%d",&t);
int start = n, end = 1;
while(t -- ){
scanf("%d",&stop);//输入的是编号
start = min(start, stop);
end = max(end, stop);
vis[stop] = true;//代表该站要停靠.
}
int source = n + i;//n + 1
for(int j = start;j <= end;++j)
{//该线路上的所有经过的站点的编号一定在始发站和终点站之间
if(vis[j])//要停靠,说明是右部点
add(source, j, 1);
else add(j, source, 0);
}
}
toposort();
for(int i = 1;i <= n;++i)//A ≥ 1
dist[i] = 1;
for(int i = 0;i < n + m;++i){//手写的队列是从0开始的
int x = q[i];
for(int j = head[x];~j;j = nex[j]){
int y = ver[j], z = edge[j];
dist[y] = max(dist[y], dist[x] + z);
}
}
int res = 0;
for(int i = 1;i <= n;++i)//满足所有约束条件的解中最大值既是题目要求的最高的级别
res = max(res, dist[i]);
printf("%d\n",res);
return 0;
}