目录
因为它会让你重拾人生方向
(只要别向着图论走去哪都行) __题记
图的存储
-
总纲
- 邻接矩阵:O(1)查询、O(n^2)遍历
- 边表:O(n)查询、O(n)遍历
邻接矩阵
map[i][j]=val;
前向星
struct node
{
int to;
int next;
int w;
node(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxn2];
void add(int x,int y,int v)
{
e[++cnt]=(node){y,head[x],v};
head[x]=cnt;
}
Topsort
P1038 神经网络
题目很水,就是有点坑
不难发现这是个DAG,且个点的更新有严格的顺序要求(因此不能化点为边)
由此想到用topsort边遍历边更新点权
输出层的出度为0,因此只要读入时处理一下即可,不用更新
至于阈值,由于他不在sigma中,因此只要是非输入层直接处理
细节坑点见代码
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#define qwq 0
namespace ljm
{
int dalao;
}
int const maxn=111,maxn2=20110;
int head[maxn],ind[maxn];
int cnt;
int a[maxn],oud[maxn],instack[maxn];
std::queue<int>q;
struct node
{
int to;
int next;
int w;
node(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxn2];
void add(int x,int y,int v)
{
e[++cnt]=(node){y,head[x],v};
head[x]=cnt;
}
int n,m;
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int U;
scanf("%d%d",&a[i],&U);
if(!a[i])
//直接处理阈值
a[i]-=U;
else
{
q.push(i);
instack[i]=true;
}
}
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
ind[y]++;
oud[x]++;
}
}
void topsort()
{
while(!q.empty())
{
int u=q.front();
q.pop();
instack[u]=false;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(a[u]>0)
{
//不活跃的点不能传递信息
a[v]+=a[u]*e[i].w;
}
ind[v]--;
if(!ind[v]&&!instack[v])
//instack[]表示队内标记,防止重复入队
{
instack[v]=true;
q.push(v);
}
}
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
topsort();
for(int i=1;i<=n;i++)
{
if(!oud[i]&&a[i]>0)
ljm::dalao=true;
}
if(!ljm::dalao)
{
printf("NULL");
return 0;
}
for(int i=1;i<=n;i++)
if(!oud[i]&&a[i]>0)
//这是个超级大坑点
//输出层(广义上的,指的是出度为0的点)无法传递信息相当于无法输出(就不能算作输出层了)
printf("%d %d\n",i,a[i]);
return 0;
}
P1983 车站分级
感觉做图论越来越有感觉了,很清楚自己写了什么,改了一遍就过了,又立flag!
喜闻乐见的语文题,关键在于建图
选中的车站一定大于没选中的,因此如果在其他车次中要选没选中的,选中的一定会被选,这显然跟topsort序有关
由于要求最少等级,我们可以将<=全看作=
举个例子,i1 i2都连向j,可知i1,i2<=j,此时i1==i2肯定是最优的
因此题目就转化成了求最长的topsort链(就是上题 神经网络的“层数lev”)
建图是个难题,细节见代码
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<utility>
int const maxn=1011,maxn2=1011;
struct node
{
int to,next;
node(int to=0,int next=0):to(to),next(next){}
}e[maxn*maxn2];
struct node2
{
int nd;
int lev;
node2(int nd=0,int lev=0):
nd(nd),lev(lev){}
};
int a[maxn][maxn2],vis[maxn][maxn2],ind[maxn],cnt,ans,n,m,head[maxn];
int pan[maxn][maxn];
void add(int u,int v)
{
e[++cnt]=(node){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
//共m个车次
scanf("%d",&a[i][0]);
//a[i][0]存第i车次的站数
for(int j=1;j<=a[i][0];j++)
{
scanf("%d",&a[i][j]);
//a[i][j]表示第i车次的第j站
vis[i][a[i][j]]=true;
}
for(int j=a[i][1];j<=a[i][a[i][0]];j++)
//搜索第i车次起点到终点的所有站点
{
if(vis[i][j])
//只找未被选中的站点
continue;
for(int k=1;k<=a[i][0];k++)
//找选中的站点
//令j向k连边
{
int v=a[i][k];
if(!pan[j][v])
{
add(j,v);
ind[v]++;
pan[j][v]=true;
}
}
}
}
}
void topsort()
{
std::queue<node2>q;
for(int i=1;i<=n;i++)
if(!ind[i])
q.push(node2(i,1));
// inque[i]=true;
while(!q.empty())
{
node2 u=q.front();
q.pop();
// inque[i]=false;
for(int i=head[u.nd];i!=-1;i=e[i].next)
{
int v=e[i].to;
ind[v]--;
if(!ind[v])
{
q.push(node2(v,u.lev+1));
ans=std::max(ans,u.lev+1);
}
}
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
topsort();
printf("%d",ans);
return 0;
}
最小生成树 MST
-
总纲
- prim O((n+m)*logm)
- kruskal O(m*logm)
- 总结:kruskal能做的题prim不一定能做,prim能做的题kruskal一定能做
- 解题思路:先建一个mst,考虑用非树边代替树边
堆优化Prim
用前向星存边
把每个点扩展到的边压成点放进堆里
根据优先队列的性质,就能求出集合内到集合外的最小距离
正是因为这个,我们就不用for1-n来求last了…
如果已经在集合内,直接continue,因此无脑压入队列即可
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
int const maxn=5100,maxn2=200100;
int cost[maxn],vis[maxn],cnt,sum,head[maxn],n,m,num;
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxn2<<1];
struct node
{
int nd,dis;
bool operator<(const node &b)const
{
return dis>b.dis;
}
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
void add(int x,int y,int w)
{
e[++cnt]=(E){y,head[x],w};
head[x]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
add(u,v,w);
add(v,u,w);
}
}
void prim()
{
std::priority_queue<node>q;
q.push(node(1,0));
cost[1]=0;
while(!q.empty())
{
int u=q.top().nd;
int val=q.top().dis;
q.pop();
if(vis[u])
continue;
num++;
sum+=val;
vis[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(e[i].w<cost[v])
//cost[i]表示从集合内到i的最短距离
{
cost[v]=e[i].w;
q.push(node(v,e[i].w));
}
}
}
}
void write()
{
if(num<0)
{
printf("orz");
return;
}
printf("%d",sum);
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,0x7f7f7f7f,sizeof(cost));
readin();
prim();
write();
return 0;
}
并查集Kruskal
无脑贪心
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
int const maxn=5100,maxn2=210100;
int fa[maxn],num,ans,n,m;
struct RE
{
int u,v,w;
}e[maxn2<<1];
int find(int x)
{
if(fa[x]==x)
return x;
return fa[x]=find(fa[x]);
}
int cmp(RE a,RE b)
{
return a.w<b.w;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
scanf("%d%d%d",&e[i].u,&e[i].v,&e[i].w);
}
void kruskal()
{
std::sort(e+1,e+1+m,cmp);
for(int i=1;i<=m;i++)
{
if(num==n-1)
break;
int x=find(e[i].u);
int y=find(e[i].v);
if(x==y)
continue;
fa[x]=y;
ans+=e[i].w;
num++;
}
}
int main()
{
readin();
kruskal();
printf("%d",ans);
return 0;
}
Matrix Tree Theory
矩阵树定理:对于有n个点的完全图,有n^(n-2)棵生成树
非严格次小生成树
求最小生成树,枚举删边,重新跑最小生成树:O(n*m)–>60分
枚举加一条边,删去环上最长边即求树链最长边 -->100分
严格次小生成树
结论:严格次小生成树有且仅有一条边与最小生成树不同
YY证法:
s–>t 在最小生成树中的边为min,在严格次小生成树中的边为cmin
易知min<cmin
如果有没有边不同是最小生成树而非次小生成树
如果有两条(及以上)边不同,设两条边为cmin1,cmin2,min1,min2
那么一定可以将找到一个有min1,cmin2或cmin1,min2的生成树
又因为min<cmin
min1+min2<cmin1+min2<cmin1+cmin2
min1+min2<min1+cmin2<cmin1+cmin2
因此min1,cmin2或cmin1,min2才是严格次小生成树
货车运输+四川省选day1 t1
CF160D edge in mst
我们定义一条非树边所构成环上的所有树边被这条非树边覆盖
枚举每条非树边,与被其覆盖的最小边比较
如果相等,则两者都可能在,如果不等,则非树边一定不在,树边一定在
bzoj 2234
P2619 Tree 1
WQS+MST
性质1:白边边权越大,在最小生成树中的白边越少
性质2:黑边与白边相对独立且白边关系不变
由性质1可知,白边在mst中的个数满足单调
因此可以直接二分白边所加的偏移量
check一下如果白边过多说明偏移量过小,过少说明偏移量过大
由性质2可知
白边为need的最小生成树的各边情况都与偏移量为x时白边为need的最小生成树相同
因此只要把x*need减去就是答案了
#include<iostream>
#include<cstdio>
#include<algorithm>
int const maxn=500100,maxm=500100;
struct RE
{
int u,v,w,color;
}a[maxm],b[maxm];
int n,m,need;
int fa[maxn],anss,ans;
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
int cmp(RE x,RE y)
{
return x.w==y.w?x.color<y.color:x.w<y.w;
//相同权值把白边放在前边,让白边更多
//感觉有锅...欢迎hack
}
int check(int val)
{
int num=0,white=0;ans=0;
for(int i=1;i<=n;i++)
fa[i]=i;
for(int i=1;i<=m;i++)
{
b[i]=a[i];
//一定要copy一下,每一次kruskal都要还原到最初才能进行
if(!b[i].color)
b[i].w+=val;
}
std::sort(b+1,b+1+m,cmp);
//********初始化*********
for(int i=1;i<=m;i++)
{
if(num==n-1)
return white>=need;
int u=b[i].u,v=b[i].v,w=b[i].w,color=b[i].color;
int fu=find(u),fv=find(v);
if(fu==fv)
continue;
fa[fu]=fv;
num++;
ans+=w,white+=color==0;
//骚操作
}
return false;
//这说明m<n-1,根本无法连通...
}
int main()
{
scanf("%d%d%d",&n,&m,&need);
for(int i=1;i<=m;i++)
{
scanf("%d%d%d%d",&a[i].u,&a[i].v,&a[i].w,&a[i].color);
a[i].u++;a[i].v++;
//坑点!点从0开始
}
int l=-120,r=120;
while(l<=r)
{
int mid=(l+r)/2;
if(check(mid))
{
anss=mid;
l=mid+1;
}
else
r=mid-1;
}
check(anss);
printf("%d",ans-need*anss);
return 0;
}
bzoj 3332 旧试题
并查集
P2700 逐个击破
背景与题目完全不符,喵的说好的链呢!
之前没有深入理解kruskal的实现思路,做完这道题有点明白了
那就是利用并查集维护某种关系来进行权值的转移
这是我做的第一道删边的并查集
详细讲讲
删边的最小代价= 总的代价-建“不能删的”边的最大代价
因此我们可以用并查集维护能建边的点,最后不在并查集的点就是敌人节点
为了保证最大,只连接一个敌人的边需要建
但这样会产生一种情况
那就是敌人可能会通过不是敌人的点进行联通
在这种情况下,想要将其加入集合,就要对该点标记,将其标记为敌人
注意这个标记可以用并查集实现
并不需要将所有与敌占区联通的点都标记,只标记并查集的树根即可
再用排序解决最优性问题就ok了
(其实从这能看出这道题可以dp)
代码也没啥细节,看看就好
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
#include<algorithm>
int const maxn2=1000008,maxn=101110;
int n,fa[maxn],anti[maxn],k;
long long ans;
struct edge
{
int u,v,w;
edge(int u=0,int v=0,int w=0):
u(u),v(v),w(w){}
}e[1000008];
int cmp(edge a,edge b)
{
return a.w>b.w;
}
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void readin()
{
scanf("%d%d",&n,&k);
for(int x,i=1;i<=k;i++)
{
scanf("%d",&x);
anti[x]=true;
}
for(int x,y,z,i=1;i<n;i++)
{
scanf("%d%d%d",&x,&y,&z);
e[i]=(edge){x,y,z};
ans+=e[i].w;
fa[i]=i;
}
}
void bcj()
{
std::sort(e+1,e+n,cmp);
for(int i=1;i<n;i++)
{
int u=find(e[i].u),v=find(e[i].v);
if(anti[u]&&anti[v])
continue;
fa[u]=v;
if(anti[u])
anti[v]=true;
ans-=e[i].w;
}
}
int main()
{
readin();
bcj();
printf("%lld",ans);
return 0;
}
Codevs1001 舒适的路线
模型:并查集维护联通问题
贪心,当最小值最接近最大值时最优
肯定不能见两个并查集,因为他们并没有什么联系
看来只能枚举maxx、minn其中之一了
显然应该枚举最大值,因为最小值<最大值,可缩小最小值的范围
贪心部分实现
for(int i=1;i<=m;i++)
{
//枚举最大值
maxx;
for(j<maxx)
{
//找最小值
if(S can reach T)
//如果起点与终点在一个联通块中
minn=e[j].w;
}
double fens=maxx/minn;
ans=std::max(ans,fens);
}
并查集维护联通块
将两个点联通转化为在同一个并查集中
类似kruskal,贪心思想进行排序,从maxx向前找,直到S T联通为止,这就是当前最大值的最优最小值(因为他们最接近)
输出比较毒瘤,详见代码
#include<iostream>
#include<cstdio>
#include<algorithm>
int const maxn=510,maxm=5100;
int fa[maxn],n,m,s,t,mx,mn,maxx,minn;
double ans=0x7f7f7f7f;
//注意处理小数
struct re
{
int u,v,w;
re(int u=0,int v=0,int w=0):
u(u),v(v),w(w){}
int operator<(const re &a)const
{
return w<a.w;
}
}e[maxm];
int find(int x)
{
return x==fa[x]?x:fa[x]=find(fa[x]);
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
fa[i]=i;
for(int u,v,w,i=1;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
e[i]=(re){u,v,w};
}
scanf("%d%d",&s,&t);
}
void bcj()
{
//用并查集模拟建边的过程
std::sort(e+1,e+1+m);
for(int i=2;i<=m;i++)
{
maxx=e[i].w;
for(int j=i;j>=1;j--)
//一定要注意,每个最大值都是独立的情况,都可能有自己的最优最小值
fa[j]=j;
for(int j=i;j>=1;j--)
{
int u=e[j].u,v=e[j].v,w=e[j].w;
int fu=find(u),fv=find(v);
if(fu==fv)
continue;
fa[fu]=fv;
if(find(s)==find(t))
{
minn=w;
break;
}
}
if(!minn)
//如果minn==0,说明该情况下st不连通
continue;
double fens=maxx/minn;
if(ans>fens)
{
ans=fens;
mx=maxx;
mn=minn;
}
}
}
int main()
{
readin();
bcj();
if(ans==0x7f7f7f7f)
{
printf("IMPOSSIBLE");
return 0;
}
if(!mx%mn||mx==mn)
{
printf("%d",mx/mn);
return 0;
}
printf("%d/%d",mx,mn);
return 0;
}
最短路
-
总纲
- 1、Floyd 多源最短路O(n^3)
- 2、dijkstra O(n^2+m)
- 3、STL堆优化dij O((n+m)*logm)
- 4、手写堆优化dij O((n+m)*logn)
- 5、斐波那契堆优化dij O(n*logn)
- 6、spfa 最坏O(n*m)
P2047 社交网络
多元最短路+像 “Cs,t(v)表示经过v从s到t的最短路的数目” 这种看起来就很Floyd的东西+n<=100
用什么算法大家想必不言自明
算法核心在于求Cs,t和Cs,t(v)
前者可以松弛
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j&&i!=k&&j!=k)
{
if(dis[i][j]>dis[i][k]+dis[k][j])
{
dis[i][j]=dis[i][k]+dis[k][j];
num[i][j]=num[i][k]*num[k][j];
//乘法原理
//如果当前松弛后更优,则最短路数应继承
continue;
}
if(dis[i][j]==dis[i][k]+dis[k][j])
num[i][j]+=num[i][k]*num[k][j];
//如果相等就说明最短路数更多了
}
后者可以利用三角形不等式
如果i到j的最短路与i到k+k到j的最短路
则说明i->j的路径上一定经过k(很显然)
所以
Cs,t(v)=sigma(i=1~n;j=1~n)num[i][k]*num[k][j](if(dis[i][j]==dis[i][k]+dis[k][j])
细节见代码
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=210;
int dis[maxn][maxn];
long long num[maxn][maxn];
//答案数<=10^10
int n,m;
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
num[x][y]=1;
num[y][x]=1;
//别忘了最短路数
dis[x][y]=z;
dis[y][x]=z;
}
}
void floyd()
{
for(int k=1;k<=n;k++)
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j&&i!=k&&j!=k)
{
if(dis[i][j]>dis[i][k]+dis[k][j])
{
dis[i][j]=dis[i][k]+dis[k][j];
num[i][j]=num[i][k]*num[k][j];
continue;
}
if(dis[i][j]==dis[i][k]+dis[k][j])
num[i][j]+=num[i][k]*num[k][j];
}
for(int k=1;k<=n;k++)
{
double ans=0;
for(int i=1;i<=n;i++)
for(int j=1;j<=n;j++)
if(i!=j&&i!=k&&j!=k)
if(dis[i][j]==dis[i][k]+dis[k][j])
{
// printf("%d,%d,%lld\n",i,j,num[i][j]);
long long sum=num[i][k]*num[k][j];
ans+=(double)sum/num[i][j];
}
printf("%.3lf\n",ans);
}
}
int main()
{
memset(dis,0x1f,sizeof(dis));
//最短路初值为最大
//最短路数初值为0
readin();
floyd();
return 0;
}
堆优化的dijkstra
与prim并没有什么不同
详见代码
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
int const maxn=100110,maxm=200100;
struct node
{
int nd,dis;
bool operator<(const node &b)const
{
return dis>b.dis;
}
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int head[maxn],cnt;
long long cost[maxn];
int n,m,s;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d%d",&n,&m,&s);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
}
}
void dijstra()
{
std::priority_queue<node>q;
q.push(node(s,0));
cost[s]=0;
while(!q.empty())
{
int u=q.top().nd;
int dis=q.top().dis;
q.pop();
if(dis!=cost[u])
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(w+dis<cost[v])
{
cost[v]=w+dis;
q.push(node(v,cost[v]));
}
}
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
cost[i]=2147483647;
dijstra();
for(int i=1;i<=n;i++)
printf("%lld ",cost[i]);
return 0;
}
P1186 玛丽卡
n<=1000,所以直接枚举最短路上的点删边即可
唯一的问题在于如何枚举“点”删“边”
我们可以用链式遍历的思想改一下前向星
用一个下标为点号的数组存边号
这样我们只要能用边号知道该边起点的点号就能完成遍历
用前向星存一下即可
while(v!=s)
v=e[way[v].from;
详见代码
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
int const maxn=1011,maxm=500110,inf=0x1f1f1f1f;
struct node
{
int nd,dis;
bool operator <(const node &b)const
{
return dis>b.dis;
}
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w,from;
//from为该边的起点
E(int to=0,int next=0,int w=0,int from=0):
to(to),next(next),w(w),from(from){}
}e[maxm<<1];
int head[maxn],cost[maxn],rway[maxn],way[maxn],re[maxn];
int n,m,ans,cnt,num;
std::priority_queue<node>q;
void pre()
{
memset(head,-1,sizeof(head));
}
void add(int u,int v,int w)
{
e[++num]=(E){v,head[u],w,u};
head[u]=num;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
add(y,x,z);
}
}
void dijstra()
{
for(int i=1;i<=n;i++)
cost[i]=inf;
//不知道怎么memset只好作此下策
q.push(node(1,0));
cost[1]=0;
while(!q.empty())
{
int u=q.top().nd;
int uv=q.top().dis;
q.pop();
if(uv!=cost[u])
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(uv+w<cost[v])
{
cost[v]=uv+w;
way[v]=i;
//记录路径的边号
q.push(node(v,cost[v]));
}
}
}
}
int main()
{
pre();
readin();
dijstra();
for(int i=1;i<=n;i++)
rway[i]=way[i];
int t=n;
while(t!=1)
{
int headu=rway[t];
int w=e[headu].w;
e[headu].w=inf;
dijstra();
ans=std::max(ans,cost[n]);
e[headu].w=w;
t=e[headu].from;
}
printf("%d",ans);
return 0;
}
P1073 最优贸易
这题模型挺好的,值得魔改
i之后的最大值-i之前的最小值能保证i解最优
然而dfs线性访问只能维护其中之一
维护最小值,当前点的利润f[i]=val[i]-i之前的最小值
我们只要边遍历边对f[i]取最大即可
由此我们很自然想到了dp
f[i]存第i个点的最大利润,mn[i]存i点前的最低价
这样就可以线性转移了
mn[i]=min(mn[last],val[i]);
f[i]=max(f[last],val[i]-mn[i]);
因为这题的更新是取最值因此满足无后效性
现在唯一的问题就是如何处理环
只有更优的点才会对环上的其他点有贡献
贡献可以线性转移,当有一个点无法更新,说明整个环更新完毕
其实这道题似乎可以把环缩成点直接当成DAG水
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=100110,maxm=500100,inf=0x1f1f1f1f;
int head[maxn],val[maxn],f[maxn],mn[maxn],vis[maxn];
int n,m;
int cnt;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&val[i]);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y);
if(z==2)
add(y,x);
}
}
void dfs(int u,int bef_min,int last)
{
int flag=0;
// printf("shit\n");
bef_min=std::min(bef_min,val[u]);
if(mn[u]>bef_min)
{
mn[u]=bef_min;
flag=true;
}
int maxx=std::max(f[last],val[u]-bef_min);
if(f[u]<maxx)
{
f[u]=maxx;
flag=true;
}
if(!flag)
return;
f[u]=std::max(f[last],val[u]-bef_min);
for(int i=head[u];i!=-1;i=e[i].next)
dfs(e[i].to,bef_min,u);
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
mn[i]=inf;
dfs(1,inf,0);
printf("%d",f[n]);
}
P1119 灾后重建
这题有一个性质
即使枚举i,j时不检查是否重建完成,也不会对当前的的询问造成影响
脑补一下
枚举的k只能是重建完成的,i,j未重建就不会成为k,即不会更新其他点,因此ij不用考虑
复杂度约为O(q*n^2)
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=1110,inf=0x1f1f1f1f;
int map[maxn][maxn],tt[maxn];
int n,m,qnum;
int s,t,tm;
int cnt;
void readin()
{
scanf("%d%d",&n,&m);
for(int i=0;i<n;i++)
//该题点号从0开始
scanf("%d",&tt[i]);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
map[x][y]=z;
map[y][x]=z;
}
scanf("%d",&qnum);
}
void floyd()
{
// sort(a,a+n,cmp);
while(qnum--)
{
scanf("%d%d%d",&s,&t,&tm);
while(tt[cnt]<=tm&&cnt!=n)
//cnt模拟指针,找当前修建完成的点松弛更新其他点
{
for(int i=0;i<n;i++)
for(int j=0;j<n;j++)
map[i][j]=std::min(map[i][j],map[i][cnt]+map[cnt][j]);
cnt++;
}
(map[s][t]==inf||tt[s]>tm||tt[t]>tm)?printf("-1\n"):printf("%d\n",map[s][t]);
//特判无解的情况
}
}
int main()
{
memset(map,0x1f,sizeof(map));
readin();
floyd();
return 0;
}
P1576 最小花费
把更新最短路的所用的+改为乘
手续费百分比越小越优
因此要跑最长路
这道水题之所以花了我1.5小时,全是我粗心,存中间运算过程的变量没开double…
#include<iostream>
#include<cstdio>
#include<queue>
#include<cstring>
int const maxn=2100,maxm=100110,inf=0x1f1f1f1f;
struct node
{
int nd;
double dis;
node(int nd=0,double dis=0):
nd(nd),dis(dis){}
int operator <(const node &a)const
{
return a.dis>dis;
//大到小排序
}
};
struct E
{
int to,next;double w;
E(int to=0,int next=0,double w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int head[maxn],cnt,vis[maxn];
double dis[maxn];
int n,m,s,t;
void add(int u,int v,double w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,i=1;i<=m;i++)
{
double z;
scanf("%d%d%lf",&x,&y,&z);
// printf("%.3lf\n",1-z/100);
add(x,y,1-z/100);
add(y,x,1-z/100);
}
scanf("%d%d",&s,&t);
}
void dijkstra()
{
std::priority_queue<node>q;
dis[s]=1;
q.push(node(s,1));
while(!q.empty())
{
int u=q.top().nd;
double val=q.top().dis;
q.pop();
if(vis[u])
//有精度误差,想了想决定还是不浪,乖乖用了标记
continue;
vis[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
double w=e[i].w;
// printf("%d : %.3lf\n",v,w);
double nowdis=val*w;
if(nowdis>dis[v])
{
dis[v]=nowdis;
q.push(node(v,dis[v]));
}
}
}
}
int main()
{
memset(head,-1,sizeof(head));
for(int i=1;i<=n;i++)
dis[i]=-inf;
//血的教训,memset无法填充double数组
readin();
dijkstra();
printf("%.8lf",100/dis[t]);
//如果输出inf说明被除数为0
return 0;
}
P1144 最短路计数
这道题思路与Floyd计数最短路一模一样
dis[v]>dis[u]继承
dis[v]==dis[u]更新
PS:状态挺不错的,一遍过了
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=1000110,maxm=2000100,mod=100003,inf=0x1f1f1f1f;
struct node
{
int nd,dis;
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
int operator<(const node &b)const
{
return dis>b.dis;
}
};
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
int head[maxn],cost[maxn],ans[maxn],cnt;
int n,m;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,i=1;i<=m;i++)
{
scanf("%d%d",&x,&y);
add(x,y);
add(y,x);
}
}
void dijkstra()
{
std::priority_queue<node>q;
cost[1]=0;
ans[1]=1;
//初始化为1
q.push(node(1,0));
while(!q.empty())
{
int u=q.top().nd;
int dis=q.top().dis;
q.pop();
if(cost[u]!=dis)
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(dis+1==cost[v])
//相等的情况
{
ans[v]+=ans[u];
ans[v]=ans[v]%mod;
}
if(dis+1<cost[v])
{
cost[v]=1+dis;
ans[v]=ans[u];
//继承是不用取模的
q.push(node(v,cost[v]));
}
}
}
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,0x1f,sizeof(cost));
readin();
dijkstra();
for(int i=1;i<=n;i++)
{
if(cost[i]==inf)
printf("0\n");
else
printf("%d\n",ans[i]);
}
return 0;
}
P3385 负环
所谓的spfa优化其实是劣化
因为fsl lll stack都抛弃了同一个点松弛操作不超过n的性质
因此复杂度都不稳定,最差O(2^n)
最朴素的bfs写法反而最差O(n^2)
数据加强后告别了几种垃圾算法,心情愉快
只是为毛这道题的连通性问题还没改!
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=30110,maxm=60110,dream=200000;
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<2];
int head[maxn],sum[maxn],cost[maxn],cnt,_1s;
int t,n,m;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
int bfs_spfa()
{
std::queue<int>q;
q.push(1);
cost[1]=0;
while(!q.empty())
{
if(++_1s>dream)
//顾名思义,梦想型优化
return true;
int u=q.front();
q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(cost[v]>cost[u]+w)
{
if(++sum[v]>n)
//这种写法不统计入队次数
//因此不需要访问标记
return true;
cost[v]=cost[u]+w;
q.push(v);
}
}
}
return false;
}
int main()
{
scanf("%d",&t);
while(t--)
{
memset(sum,0,sizeof(sum));
memset(head,-1,sizeof(head));
memset(cost,0x1f,sizeof(cost));
cnt=0;
_1s=0;
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&z);
add(x,y,z);
if(z>=0)
add(y,x,z);
}
if(bfs_spfa())
printf("YE5\n");
else
printf("N0\n");
}
return 0;
}
P1938 Job Hunt 找工就业
30min切掉的水题
化点为边+最长路+判正环
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=240,maxm=510;
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int head[maxn],cost[maxn],cnt,maxx,sum[maxn],notle;
int n,m1,m2,val,s;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d%d%d%d",&val,&m1,&n,&m2,&s);
for(int x,y,i=1;i<=m1;i++)
scanf("%d%d",&x,&y),add(x,y,val);
for(int x,y,z,i=1;i<=m2;i++)
scanf("%d%d%d",&x,&y,&z),add(x,y,val-z);
}
int spfa()
{
std::queue<int>q;
q.push(s);
cost[s]=val;
maxx=val;
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(cost[v]<cost[u]+w)
{
if(++sum[v]>n)
return true;
cost[v]=cost[u]+w,maxx=std::max(maxx,cost[v]),q.push(v);
}
}
}
return false;
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,-0x1f,sizeof(head));
readin();
if(spfa())
printf("-1");
else
printf("%d",maxx);
return 0;
}
P2865 路障Roadblocks(次短路模板题)
dijkstra求次短路
细节见代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=5100,maxm=100110;
struct node
{
int nd,dis;
int operator<(const node &b)const
{
return dis>b.dis;
}
node(int nd,int dis):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int head[maxn],cost[maxn],cnt,cost2[maxn];
int n,m;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
scanf("%d%d%d",&x,&y,&z),add(x,y,z),add(y,x,z);
}
void dijkstra()
{
std::priority_queue<node>q;
q.push(node(1,0));
cost[1]=0;
//只初始化最短路的起点
while(!q.empty())
{
int u=q.top().nd;
int dis=q.top().dis;
//dis为准备松弛更新的k短路
q.pop();
if(cost2[u]<dis)
//满足当前dis为最短路或次短路才继续更新
//因为次短路只有可能是cost[u]+w或cost2[u]+w,即只有可能由最短路或次短路更新而来
//也可以理解为这是为了保证最短路<次短路<=k短路
//dis为准备更新次短路的值,因此要保证dis<=次短路
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w,nw=w+dis;
if(cost[v]>nw)
std::swap(cost[v],nw),q.push(node(v,cost[v]));
//这个swap很精髓,省了很多麻烦,具体功能请自行理解
if(cost[v]<nw&&cost2[v]>nw)
cost2[v]=nw,q.push(node(v,cost2[v]));
}
}
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,0x1f,sizeof(cost));
memset(cost2,0x3f,sizeof(cost2));
readin();
dijkstra();
printf("%d",cost2[n]);
return 0;
}
P3905 道路重建
思路挺有意思
要求最小的修复的边
我们就把不用修复的边权赋为0
然后跑最短路
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=111,maxm=1110,inf=0x1f1f1f1f;
struct node
{
int nd,dis;
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
int operator<(const node &b)const
{
return dis>b.dis;
}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int head[maxn],cost[maxn],cnt,a[maxn][maxn];
int n,m,s,t,d;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
scanf("%d%d%d",&x,&y,&z),add(x,y,0),add(y,x,0),a[x][y]=z,a[y][x]=z;
//注意!这个邻接矩阵一定要建双向的!因为应修复的边有可能反着给你
scanf("%d",&d);
for(int x,y,i=1;i<=d;i++)
{
scanf("%d%d",&x,&y);
for(int j=head[x];j!=-1;j=e[j].next)
if(e[j].to==y)
e[j].w=a[x][y];
for(int j=head[y];j!=-1;j=e[j].next)
if(e[j].to==x)
e[j].w=a[x][y];
}
scanf("%d%d",&s,&t);
}
void dijkstra()
{
std::priority_queue<node>q;
q.push(node(s,0));
cost[s]=0;
while(!q.empty())
{
int u=q.top().nd;
int dis=q.top().dis;
q.pop();
if(dis!=cost[u])
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(cost[v]>dis+w)
cost[v]=dis+w,q.push(node(v,cost[v]));
}
}
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,0x1f,sizeof(cost));
readin();
dijkstra();
printf("%d",cost[t]);
return 0;
}
P4554 小明的游戏
这题水是水
就是二维压一维之后一定要记得把数组开大一点啊魂淡!
坐标系平移(习惯问题)+二维压一维(习惯问题)+spfa(习惯问题)
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
int const maxn=10110,maxm=10110,inf=0x1f1f1f1f;
int n,m,sx,sy,tx,ty,cost[1110*1110];
char map[1110*1110];
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[1110*1110];
int head[1110*1110],cnt;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void Bfs_spfa(int a)
{
memset(cost,0x1f,sizeof(cost));
std::queue<int>q;
cost[a]=0;
q.push(a);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(cost[v]>cost[u]+w)
cost[v]=cost[u]+w,q.push(v);
}
}
}
int main()
{
for(;;)
{
memset(map,0,sizeof(map));
memset(head,0,sizeof(head));
memset(e,0,sizeof(e));
std::cin>>n>>m;
if(!n&&!m)
return 0;
int len=n*m;
for(int i=1;i<=len;i++)
std::cin>>map[i];
for(int i=1;i<=len;i++)
{
if(i-m>0)
add(i,i-m,map[i]!=map[i-m]);
if((i-1)%m)
add(i,i-1,map[i]!=map[i-1]);
if(i+m<=len)
add(i,i+m,map[i]!=map[i+m]);
if(i%m)
add(i,i+1,map[i]!=map[i+1]);
}
std::cin>>sx>>sy>>tx>>ty;
sx++;sy++;tx++;ty++;
Bfs_spfa((sx-1)*m+sy);
std::cout<<cost[m*(tx-1)+ty]<<std::endl;
}
}
差分约束
P1993 小k的农场
差分约束+spfa判环
注意有环不一定矛盾,只有能被不停更新的环才矛盾,自行手模
所以最长路判正权,最短路判负权
图不保证连通,所以可以建个对其他点边权为0的虚点
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=10110,maxm=10110;
struct node
{
int nd,dis;
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<2];
int head[maxn],cost[maxn],vis[maxn],cnt;
int n,m;
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int op,x,y,z,i=1;i<=m;i++)
{
scanf("%d%d%d",&op,&x,&y);
if(op==1)
scanf("%d",&z),add(x,y,z);
if(op==2)
scanf("%d",&z),add(y,x,-z);
if(op==3)
add(x,y,0),add(y,x,0);
}
for(int i=1;i<=n;i++)
add(0,i,0);
//建虚电
}
int dfs_spfa(int u)
//dfs优化判环
{
vis[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(cost[u]+w>cost[v])
{
cost[v]=cost[u]+w;
if(vis[v])
//vis[v]说明有环,能被最长路的松弛更新说明是正权环
return false;
if(!dfs_spfa(v))
return false;
}
}
vis[u]=false;
return true;
}
int main()
{
memset(head,-1,sizeof(head));
memset(cost,-0x1f,sizeof(cost));
readin();
cost[0]=0;
if(!dfs_spfa(0))
printf("No");
else
printf("Yes");
return 0;
}
强联通
-
总纲
- 有向图:强连通分量
- 无向图:点双连通分量(割点:删去一个点使图不连通)、边双(桥)
- 基本思路:tarjan缩点–>DAG消灭环+topsort使得所有点有序(前指向后)+从左到右有序的dp
tarjan求强联通分量
伪代码
void Tarjan(u)
{
dfn[u]=low[u]=++index
stack.push(u)
for each (u, v) in E
{
if (v is not visted)
{
tarjan(v)
low[u] = min(low[u], low[v])
}
else if (v in stack)
{
low[u] = min(low[u], dfn[v])
}
}
if (dfn[u] == low[u])
{
repeat
v = stack.pop
print v
until (u== v)
}
}
//复杂度O(n+m)
dfn[]为时间戳,low[u]为u能到达的最早的时间戳
每访问一个新点,就将其压入栈中
若不在栈内,就比较low的值,因为u可达v,v能到达的最早的点u也能到达
若那个点已经在栈内了,说明该点比当前点早,若未成功更新,说明当前点与比该点更早的点联通
若在回溯中发现有点u点的dfn与low相等,说明u没被更新也即能到达u的点中没有点比u更早,因此u即该强连通分量在搜索树中的根
将u即u以上的点出栈作为一个强联通分量
void tarjan(int u)
{
low[u]=dfn[u]=++cnt;
s.push(u);
instack[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
//用dfn充当访问标记
tarjan(v),low[u]=std::min(low[u],low[v]);
else
if(instack[v])
low[u]=std::min(low[u],dfn(v));
}
if(dfn[u]==low[u])
for(;;)
{
int v=s.top();
instack[v]=false;
printf("%d",v);
s.pop();
if(u==v)
break;
}
}
P1726 上白泽慧音
裸的tarjan求强联通
就是字典序很烦
不过这道题可以取巧
因为同一个点不会出现在多个强连通分量中,因此只要在强联通的点的数量相同时判断其最小点号的字典序大小即可
详见代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<stack>
#include<algorithm>
int const maxn=5100,maxm=100110;
struct node
{
int num,nd[maxn];
int operator<(const node &b)const
{
return num==b.num?nd[1]<b.nd[1]:num>b.num;
//比较有趣,请自行理解
}
}ans[maxn];
//结构体内的运算符重载只会影响结构体数组,对结构体内的子数组无影响
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
int head[maxn],cnt,num,low[maxn],dfn[maxn],instack[maxn],tot;
int n,m;
std::stack<int>s;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,op,i=1;i<=m;i++)
{
scanf("%d%d%d",&x,&y,&op);
add(x,y);
if(op==2)
add(y,x);
}
}
void tarjan(int u)
{
low[u]=dfn[u]=++num;
s.push(u);
instack[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
tarjan(v),low[u]=std::min(low[u],low[v]);
else
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
{
for(tot++;;)
{
int v=s.top();
ans[tot].nd[++ans[tot].num]=v;
s.pop();
instack[v]=false;
if(u==v)
break;
}
std::sort(ans[tot].nd+1,ans[tot].nd+1+ans[tot].num);
//按点号排序
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
std::sort(ans+1,ans+1+tot);
printf("%d\n",ans[1].num);
for(int i=1;i<=ans[1].num;i++)
printf("%d ",ans[1].nd[i]);
return 0;
}
P3388 模板割点
因为一个点只会被访问一遍,所以如果从u开始访问,就只能通过子树大于等于2来判断割点
而如果不从u开始访问,就可以通过判断他的v是否连向比low[u]更早的点,若是就是割点
坑点:一个点可能被重复判为割点,因此答案统计时要注意
#include<iostream>
#include<cstdio>
#include<cstring>
int const maxn=100110,maxm=100110;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
int head[maxn],cnt,num,low[maxn],dfn[maxn],cut[maxn],ans,vis[maxn];
int n,m;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,i=1;i<=m;i++)
scanf("%d%d",&x,&y),add(x,y),add(y,x);
}
void tarjan(int u,int root)
{
int child=0;
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v,root);
low[u]=std::min(low[u],low[v]);
if(u!=root&&low[v]>=dfn[u])
{
cut[u]=true;
vis[u]++;
if(vis[u]==1)
ans++;
}
if(u==root)
child++;
}
low[u]=std::min(low[u],dfn[v]);
}
if(child>=2)
cut[root]=true,ans++;
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i,i);
printf("%d\n",ans);
for(int i=1;i<=n;i++)
if(cut[i])
printf("%d ",i);
return 0;
}
P3387 模板缩点
缩点即将一个强联通分量视为一个点
若两个不同属同一强联通分量的点有边,则按相同方向对这两个“点”连边
缩完点就可以在DAG上跑dp了
详见代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
int const maxn=100110,maxm=100110,inf=0x7f7f7f7f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
int head[maxn];
int low[maxn],dfn[maxn],color[maxn],instack[maxn];
std::stack<int>s;
int vis[maxn],sum[maxn],f[maxn],ans=-inf;
int n,m,x[maxn],y[maxn],val[maxn];
int cnt,num,cur;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
scanf("%d",&val[i]);
for(int i=1;i<=m;i++)
scanf("%d%d",&x[i],&y[i]),add(x[i],y[i]);
}
void tarjan(int u)
{
dfn[u]=low[u]=++num;
s.push(u),instack[u]=true;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
{
tarjan(v);
low[u]=std::min(low[u],low[v]);
}
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
//********缩点***********
if(dfn[u]==low[u])
for(cur++;;)
{
int v=s.top();
instack[v]=false,s.pop();
color[v]=cur;
//标记每个点所属的强连通分量
sum[cur]+=val[v];
//每个super point的点权
if(v==u)
break;
}
}
//***********重建边************
void rebuild()
{
memset(head,-1,sizeof(head));
memset(e,0,sizeof(e));
cnt=0;
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
add(color[x[i]],color[y[i]]);
}
void dfs(int u)
{
if(f[u])
return;
f[u]=sum[u];
int maxx=0;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!f[v])
dfs(v);
maxx=std::max(maxx,f[v]);
}
f[u]+=maxx;
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
rebuild();
for(int i=1;i<=cur;i++)
{
if(!f[i])
dfs(i);
ans=std::max(ans,f[i]);
}
printf("%d",ans);
return 0;
}
P1262 间谍网络
tarjan缩点+入度判断+无解判断
当一个点既无法被收买又无法被别的店访问,即为无解
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
int const maxn=200110,maxm=200110,inf=0x7f7f7f7f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
int head[maxn];
int low[maxn],dfn[maxn],color[maxn],instack[maxn];
std::stack<int>s;
int sum[maxn],ind[maxn],ans;
int n,rn,m,x[maxn],y[maxn],val[maxn];
int cnt,num,cur;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&rn);
for(int u,z,i=1;i<=rn;i++)
scanf("%d%d",&u,&z),val[u]=z;
scanf("%d",&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&x[i],&y[i]),add(x[i],y[i]);
}
void tarjan(int u)
{
s.push(u),instack[u]=true;
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
tarjan(v),
low[u]=std::min(low[u],low[v]);
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
for(cur++;;)
{
int v=s.top();
s.pop(),instack[v]=false;
color[v]=cur;
if(val[v])
sum[cur]=std::min(val[v],sum[cur]);
if(u==v)
break;
}
}
void rebuild()
{
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
ind[color[y[i]]]++;
}
int main()
{
memset(sum,0x1f,sizeof(sum));
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i]&&val[i])
tarjan(i);
for(int i=1;i<=n;i++)
if(!dfn[i])
{
printf("NO\n%d",i);
return 0;
}
rebuild();
for(int i=1;i<=cur;i++)
if(!ind[i])
ans+=sum[i];
printf("YES\n%d",ans);
return 0;
}
P2002 消息扩散
tarjan缩点+topsort
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
int const maxn=1001100,maxm=5010000,inf=0x7f7f7f7f;
struct node
{
int nd,dis;
int operator <(const node &b)const
{
return dis>b.dis;
}
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
std::stack<int>s;
int n,m,x[maxn],y[maxn];
int head[maxn];
int dfn[maxn],low[maxn],ind[maxn],color[maxn],instack[maxn];
int cur,num,cnt,ans;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&x[i],&y[i]);
if(x[i]!=y[i])
add(x[i],y[i]);
}
}
void tarjan(int u)
{
s.push(u),instack[u]=true;
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
tarjan(v),low[u]=std::min(low[u],low[v]);
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
for(cur++;;)
{
int v=s.top();
s.pop(),instack[v]=false;
color[v]=cur;
if(u==v)
break;
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
ind[color[y[i]]]++;
for(int i=1;i<=cur;i++)
if(!ind[i])
ans++;
printf("%d",ans);
return 0;
}
P2341 受欢迎的牛
当出度为0的点有不止一个时无解
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#include<stack>
int const maxn=101100,maxm=500100,inf=0x7f7f7f7f;
struct E
{
int to,next;
E(int to=0,int next=0):
to(to),next(next){}
}e[maxm<<1];
std::stack<int>s;
int head[maxn],n,m;
int dfn[maxn],low[maxn],instack[maxn],color[maxn],ton[maxn];
int x[maxn],y[maxn];
int oud[maxn];
int cnt,cur,num;
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&x[i],&y[i]),add(x[i],y[i]);
}
void tarjan(int u)
{
s.push(u),instack[u]=true;
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
tarjan(v),
low[u]=std::min(low[u],low[v]);
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
for(cur++;;)
{
int v=s.top();
s.pop(),instack[v]=false;
ton[cur]++;
color[v]=cur;
if(u==v)
break;
}
}
int main()
{
memset(head,-1,sizeof(head));
readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
tarjan(i);
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
oud[color[x[i]]]++;
int last,flag=0;
for(int i=1;i<=cur;i++)
if(!oud[i])
{
if(flag)
{
last=0;
break;
}
last=i,flag=true;
}
printf("%d",ton[last]);
return 0;
}
匹配
-
二分图匹配(匹配:左与右连边)(在每一个点只匹配一个点的前提下求最多匹配)
- 匈牙利算法 复杂度上界:(邻接矩阵:O(n^3) 边表:O(n*m))
- dinic 二分图模型:树(奇数深度连向偶数深度)、网格图(每个图连向周围四个点)
- ` 网络流:
- 网络流24题+2009年胡伯涛论文<最小割模型在信息学竞赛中的应用> 费用流
- `
最大团(最多的点两两有边)=补图的最大独立集(两两无边)
-
总纲
- 一般图:NPC问题(n<=20)
- 特殊图(非NPC):二分图
最小割最大流定理
最大独立集=左点+右点-最大团
P2423朋友圈
优质题目
绝不跟上面的水货同流合污
P3119 草鉴定
卡细节卡了4节课…
思路还是很小清新的
先考虑缩点
然后记录起点所在的强连通分量能访问到的点和能访问到起点所在的强连通分量的点(通过建反图实现)
然后枚举每个点及其能到达的点,如果该点和能到达的点分属于正反图中,符合题意,max统计答案即可
#include<iostream>
#include<cstdio>
#include<cstring>
#include<stack>
#include<queue>
int const maxn=100110,maxm=100110;
struct node
{
int nd,dis;
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
std::stack<int>s;
int head[maxn];
int instack[maxn],dfn[maxn],low[maxn],color[maxn],sum[maxn];
int n,m,x[maxn],y[maxn];
int cnt,num,cur,ans,start;
int cost[maxn],val[maxn];
void add(int u,int v)
{
e[++cnt]=(E){v,head[u]};
head[u]=cnt;
}
void Readin()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
scanf("%d%d",&x[i],&y[i]),add(x[i],y[i]);
}
void Tarjan(int u)
{
s.push(u),instack[u]=true;
dfn[u]=low[u]=++num;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to;
if(!dfn[v])
Tarjan(v),low[u]=std::min(low[u],low[v]);
if(instack[v])
low[u]=std::min(low[u],dfn[v]);
}
if(dfn[u]==low[u])
for(cur++;;)
{
int v=s.top();
s.pop(),instack[v]=false;
color[v]=cur;
sum[cur]++;
if(u==v)
break;
}
}
void Rebuild()
{
cnt=0;
memset(e,0,sizeof(e));
memset(head,-1,sizeof(head));
memset(cost,0,sizeof(cost));
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
add(color[x[i]],color[y[i]]);
start=color[1];
cost[start]=sum[start];
}
void _Rebuild()
{
cnt=0;
memset(cost,0,sizeof(cost));
memset(e,0,sizeof(e));
memset(head,-1,sizeof(head));
for(int i=1;i<=m;i++)
if(color[x[i]]!=color[y[i]])
add(color[y[i]],color[x[i]]);
start=color[1];
cost[start]=sum[start];
}
void Bfs_Spfa()
{
std::queue<int>q;
q.push(start);
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=sum[v];
if(cost[v]<cost[u]+w)
cost[v]=cost[u]+w,q.push(v);
}
}
}
int main()
{
memset(head,-1,sizeof(head));
Readin();
for(int i=1;i<=n;i++)
if(!dfn[i])
Tarjan(i);
Rebuild();
Bfs_Spfa();
for(int i=1;i<=cur;i++)
val[i]=cost[i];
_Rebuild();
Bfs_Spfa();
ans=sum[start];
for(int i=1;i<=n;i++)
{
int u=color[i];
if(!val[u])
continue;
for(int j=head[u];j!=-1;j=e[j].next)
{
int v=e[j].to;
if(!cost[v])
continue;
ans=std::max(ans,val[u]+cost[v]-sum[start]);
}
}
printf("%d",ans);
return 0;
}
P2446 大陆争霸
这个题属于带限制的dijkstra问题
限制条件的处理可以运用类似topsort的思想,存某个点的入度与该点保护的点,只有入度减为零才入队
以下为贪心正确性的脑补
某个点最终的时间为到该点的最短路与到保护该点的点的最短路的max
因为每次保护点v的点的每次出队会令v的入队减少,而dijkstra的出队是从小到大的
因此假设有一个点u出队使得v的入度减为0而入队,那么到的最短路u一定是到所有保护v的点中最大的那个
因此只要对到u最短路和到v最短路取个max即为u的最小用时
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
int const maxn=3111,maxm=70110,inf=0x7f7f7f7f;
struct node
{
int nd,dis;
int operator <(const node &b)const
{
return dis>b.dis;
}
node(int nd=0,int dis=0):
nd(nd),dis(dis){}
};
struct E
{
int to,next,w;
E(int to=0,int next=0,int w=0):
to(to),next(next),w(w){}
}e[maxm<<1];
int n,m;
int head[maxn],ind[maxn],a[maxn][maxn],cnt;
int c1[maxn],c2[maxn];
void add(int u,int v,int w)
{
e[++cnt]=(E){v,head[u],w};
head[u]=cnt;
}
void readin()
{
scanf("%d%d",&n,&m);
for(int x,y,z,i=1;i<=m;i++)
scanf("%d%d%d",&x,&y,&z),add(x,y,z);
for(int i=1;i<=n;i++)
{
scanf("%d",&ind[i]);
for(int x,j=1;j<=ind[i];j++)
{
scanf("%d",&x);
a[x][++a[x][0]]=i;
}
}
}
void dijkstra()
{
std::priority_queue<node>q;
q.push(node(1,0));
c1[1]=0;
//不用考虑c2的初始化问题,因为c2的转移不考虑到该点最短路的更新
while(!q.empty())
{
int u=q.top().nd;
int dis=q.top().dis;
q.pop();
if(dis!=std::max(c1[u],c2[u]))
continue;
for(int i=head[u];i!=-1;i=e[i].next)
{
int v=e[i].to,w=e[i].w;
if(c1[v]>dis+w)
{
c1[v]=dis+w;
if(!ind[v])
//若入度不为零就只更新不入队,这样通过可确保将来2途径入队后的最短路正确
q.push(node(v,std::max(c1[v],c2[v])));
}
}
for(int i=1;i<=a[u][0];i++)
{
int v=a[u][i];
ind[v]--;
if(!ind[v])
c2[v]=dis,q.push(node(v,std::max(c1[v],c2[v])));
//这里的c2一定正确
}
}
}
int main()
{
memset(c1,0x1f,sizeof(c1));
memset(head,-1,sizeof(head));
readin();
dijkstra();
printf("%d",std::max(c1[n],c2[n]));
return 0;
}