目录
写在前面
1.图的建立和遍历
2.最短路
3.拓扑排序
4.生成树
5.连通块
6.二分图,网络流等
这篇文章主要总结部分NOIP的图论知识点以及一些NOIP真题。
图的存储方式主要就是邻接矩阵和前向星这两种,这里不多赘述。
遍历说白了就是纯粹的暴力。。。。但也是一种很重要的拿部分分的方式。。。
主要就是深度优先搜索和广度优先搜索两种搜索方式
纯考这个的好像就一道信息传递吧。。虽然很水就是了。不过2003年的传染病控制我觉得很迷。。。
直接dfs找最小环即可,连tarjan都不用因为不用存储环
#include
using namespace std;
const int MAXN=2e5+10;
const int INF=0x3f3f3f3f;
int n;
int to[MAXN],dep[MAXN],num[MAXN];
int tot,ans=INF;
int Read(){
int i=0,f=1;
char c;
for(c=getchar();(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-')
f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar())
i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void dfs(int u,int d)
{
dep[u]=d;
num[u]=tot;
if(dep[to[u]]){
if(num[to[u]]
求最短路的方法主要就是SPFA和迪杰斯特拉两种,由于前面一种太容易被卡成O(nm)的复杂度,所以一般推荐堆优化的迪杰斯特拉,基本稳定O((n+m)logn)
而需要用Floyd的题不多,需要结合数据范围来看,如果数据范围较小且多次询问两点间最短路,可能需要上Floyd,另外这个也有传递闭包的作用(但CSP选手表示不会)
NOIP近年有关题目:
2017DAY1T3逛公园,2013DAY2T3华容道,2012DAY2T2寻找道路
可以看见一旦考最短路,位置都是在T3位置,但寻找道路这道题真的太水了,让我怀疑这个DAY2T2的水准。
但前面两道都很有意思,第一题侧重于最短路上的DP,第二道则是更侧重于建图,而经典题目如墨墨的等式这种,都是在最短路的基础之上加上数论或者DP,所以刷题时可以侧重这方面。
首先跑出1到所有点的最短路,记作dis[i]。
定义状态方程dp[x][i]表示到点x比最短路大i的路径条数,根据这个数据规模明显直接O(nk)枚举就行。
DP转移方程:,这里因为一个点可以被多次搜到,所以记忆化可以做到剪枝效果。而后面这个式子看着长其实移项后就是,其中表示如果走这条边比最短路长多少,然后用当前枚举长的长度减去这么一坨就是剩下的长度,就是a。
#include
using namespace std;
const int MAXN=1e5+10;
const int MAXM=2e5+10;
const int INF=0x3f3f3f3f;
#define pii pair
int T;
int n,m,p,k,cnt,flag,ans;
int head[MAXN],viss[MAXN],dis[MAXN],dp[MAXN][51],vis[MAXN][51];
int headd[MAXN];
int nxt[MAXM],to[MAXM],w[MAXM];
int nxtt[MAXM],too[MAXM],ww[MAXM];
int Read(){
int i=0,f=1;
char c=getchar();
for(;(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-') f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar()) i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void add(int x,int y,int z){
++cnt;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
w[cnt]=z;
nxtt[cnt]=headd[y];
headd[y]=cnt;
too[cnt]=x;
ww[cnt]=z;
}
void dij(int s){
memset(dis,INF,sizeof(dis));
dis[s]=0;
priority_queue q;
q.push(make_pair(0,s));
while(!q.empty()){
int u=q.top().second;
q.pop();
// viss[u]=0;
for(int i=head[u];i!=-1;i=nxt[i]){
int v=to[i];
if(dis[u]+w[i]
利用bfs建图,因为nm都较小所以可以接受重构整个图然后跑最短路(码量稍大)
#include
using namespace std;
const int INF=0x3f3f3f3f;
const int MAXT=32;
const int MAXN=3620;
const int MAXM=MAXN*5;
int n,m,p;
bool a[MAXT][MAXT],vis[MAXN];
int dx[4]={-1,0,1,0};
int dy[4]={0,1,0,-1};
int predis[MAXT][MAXT],dis[MAXN];
struct game{
int nxt,to,w;
}edge[MAXM];
int top,head[MAXN];
void add(int x,int y,int z)
{
top++;
edge[top].nxt=head[x];
head[x]=top;
edge[top].to=y;
edge[top].w=z;
}
struct start{
int x,y;
}cur,nt;
queue q;
queue que;
int numbers(int i,int j)
{
j--;
return (i-1)*m+j<<2;
}
void bfs(int ex,int ey,int px,int py,int d)
{
int cx,cy,nx,ny;
memset(predis,-1,sizeof(predis));
predis[ex][ey]=0;
predis[px][py]=1;
cur.x=ex,cur.y=ey;
q.push(cur);
while(!q.empty())
{
cur=q.front();
q.pop();
cx=cur.x,cy=cur.y;
for(int i=0;i<4;++i)
{
nx=cur.x+dx[i],ny=cur.y+dy[i];
if(a[nx][ny]&&predis[nx][ny]==-1)
{
predis[nx][ny]=predis[cx][cy]+1;
nt.x=nx,nt.y=ny;
q.push(nt);
}
}
}
if(d==8)
return ;
int tmp=numbers(px,py);
for(int i=0;i<4;++i)
{
int x=px+dx[i],y=py+dy[i];
if(predis[x][y]>0)
add(tmp+d,tmp+i,predis[x][y]);
}
add(tmp+d,numbers(ex,ey)+(d+2)%4,1);
}
void SPFA(int sx,int sy)
{
int tmp;
memset(dis,-1,sizeof(dis));
for(int i=0;i<4;++i)
{
int x=sx+dx[i],y=sy+dy[i];
if(predis[x][y]!=-1)
{
tmp=numbers(sx,sy)+i;
dis[tmp]=predis[x][y];
que.push(tmp);
}
}
int u;
while(!que.empty())
{
u=que.front();
que.pop();
vis[u]=false;
for(int i=head[u];i;i=edge[i].nxt)
{
int v=edge[i].to;
if(dis[v]==-1||dis[v]>dis[u]+edge[i].w)
{
dis[v]=dis[u]+edge[i].w;
if(!vis[v])
{
vis[v]=true;
que.push(v);
}
}
}
}
}
int main()
{
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
scanf("%d",&a[i][j]);
for(int i=1;i<=n;++i)
for(int j=1;j<=m;++j)
if(a[i][j])
{
if(a[i-1][j])
bfs(i-1,j,i,j,0);
if(a[i][j+1])
bfs(i,j+1,i,j,1);
if(a[i+1][j])
bfs(i+1,j,i,j,2);
if(a[i][j-1])
bfs(i,j-1,i,j,3);
}
int ex,ey,sx,sy,tx,ty,ans;
while(p--)
{
scanf("%d%d%d%d%d%d",&ex,&ey,&sx,&sy,&tx,&ty);
if(sx==tx&&sy==ty)
{
printf("0\n");
continue;
}
bfs(ex,ey,sx,sy,8);
SPFA(sx,sy);
ans=INF;
int tmp=numbers(tx,ty);
for(int i=0;i<4;++i)
{
if(dis[tmp+i]!=-1)
ans=min(ans,dis[tmp+i]);
}
if(ans==INF)
printf("-1\n");
else
printf("%d\n",ans);
}
return 0;
}
很水,博客传送门
至于k短路和差分约束这种。。。。已经不是NOIP考点了。。。(但关我CSP什么事)
k短路目前还没做过几道题,需加强。
例题是魔法猪学院,但A_star算法会被洛谷数据卡掉
板子:
#include
using namespace std;
const int MAXN=5e3+10;
const int MAXM=4e5+10;
const int INF=0x3f3f3f3f;
int n,m,cnt,cnt1,ans;
int head[MAXN],head1[MAXN],vis[MAXN];
double e,dis[MAXN];
int nxt[MAXM],to[MAXM];
double w[MAXM],w1[MAXM];
int nxt1[MAXM],to1[MAXM];
priority_queue > q;
struct node{
int x;
double dist;
friend inline bool operator<(const node &a,const node &b){
return dis[a.x]+a.dist>dis[b.x]+b.dist;
}
};
int Read(){
int i=0,f=1;
char c=getchar();
for(;(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-') f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar()) i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void add(int x,int y,double z){
++cnt;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
w[cnt]=z;
}
void add1(int x,int y,double z){
++cnt1;
nxt1[cnt1]=head1[x];
head1[x]=cnt1;
to1[cnt1]=y;
w1[cnt1]=z;
}
void dij(){
for(int i=1;i<=n;++i) dis[i]=1000000000;
dis[n]=0;
q.push(make_pair(0,n));
while(!q.empty()){
int u=q.top().second;
q.pop();
for(int i=head1[u];i!=-1;i=nxt1[i]){
int v=to1[i];
if(dis[v]>dis[u]+w1[i]){
dis[v]=dis[u]+w1[i];
q.push(make_pair(-dis[v],v));
}
}
}
}
priority_queue q1;
void a_star(){
q1.push((node){1,0});
while(!q1.empty()){
node x=q1.top();
q1.pop();
if(x.x==n){
e-=x.dist;
if(e<0) return ;
++ans;
continue;
}
int u=x.x;
for(int i=head[u];i!=-1;i=nxt[i]){
int v=to[i];
q1.push((node){v,x.dist+w[i]});
}
}
}
int main(){
memset(head,-1,sizeof(head));
memset(head1,-1,sizeof(head1));
n=Read(),m=Read();
scanf("%lf",&e);
for(int i=1;i<=m;++i){
int x=Read(),y=Read();
double z;
scanf("%lf",&z);
add(x,y,z),add1(y,x,z);
}
dij();
// for(int i=1;i<=n;++i) printf("%.1lf ",dis[i]);
a_star();
cout<
差分约束就是SPFA的拓展,因为存在负边权所以dij可能会挂,典型例题就是SCOI2011糖果
考虑建图,如果X为1,那么就由A向B连一条边权为0的边,再由B向A连一条边权为0的边,就相当于限制A<=B,B<=A,这样一定能满足A=B的情况。
如果X为2,首先如果A=B,那么肯定就非法了,因为自己不能小于自己,然后就从B向C连一条边权为1的边,限制C-B>=1。
如果X为3,就由C向B连一条容量为0的边,限制C-B>=0。
如果X为4,还是先特判一下是否相等,然后从C向B连一条边权为1的边。
如果X为5,由B向C连一条容量为0的边。
建立一个起点s,由s向每个点连一条边权为1的边(因为每个人至少有1个糖果)。
最后跑一个最长路,如果出现环就退出,否则输出所有dis之和。
#include
using namespace std;
typedef long long LL;
const LL MAXN=1e5+10;
const LL MAXM=1e6+10;
const LL INF=0x3f3f3f3f;
LL n,m,cnt,s,t;
LL head[MAXN],dis[MAXN],vis[MAXN],sum[MAXN];
LL nxt[MAXM],to[MAXM],w[MAXM];
LL Read()
{
LL i=0,f=1;
char c;
for(c=getchar();(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-')
f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar())
i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void add(LL x,LL y,LL z)
{
cnt++;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
w[cnt]=z;
}
bool SPFA()
{
memset(dis,0,sizeof(dis));
memset(vis,0,sizeof(vis));
queue q;
dis[s]=0;
vis[s]=1;
q.push(s);
while(!q.empty())
{
LL u=q.front();
q.pop();
vis[u]=0;
for(LL i=head[u];i!=-1;i=nxt[i])
{
LL v=to[i];
if(dis[v]=n)
return true;
q.push(v);
}
}
}
}
return false;
}
int main()
{
memset(head,-1,sizeof(head));
n=Read(),m=Read();
while(m--)
{
LL a=Read(),b=Read(),c=Read();
switch(a)
{
case 1:{
add(b,c,0);
add(c,b,0);
break;
}
case 2:{
if(b==c)
{
puts("-1");
return 0;
}
add(b,c,1);
break;
}
case 3:{
add(c,b,0);
break;
}
case 4:{
if(b==c)
{
puts("-1");
return 0;
}
add(c,b,1);
break;
}
case 5:{
add(b,c,0);
break;
}
}
}
for(LL i=n;i>0;--i)
add(0,i,1);
s=0;
if(SPFA())
{
puts("-1");
return 0;
}
LL ans=0;
for(LL i=1;i<=n;++i)
ans+=dis[i];
cout<
拓扑排序的坑点就是你要考虑正向建图还是反向建图以及正确性,NOIP好像也就2003年的神经网络考了。。可见NOIP水准没法怎么出这方面的题。
过程比较简单,就是在有向图中不断删去入度为0的点(利用队列实现),最后剩下的点会构成环(如植物大战僵尸一题的建图过程)
典型例题可以看看洛谷1137 旅行计划,由于比较简单直接粘代码
#include
using namespace std;
const int MAXN=1e5+10;
const int MAXM=4e5+10;
const int INF=0x3f3f3f3f;
int n,m,cnt,s;
int head[MAXN],du[MAXN],dis[MAXN];
int nxt[MAXM],to[MAXM];
void add(int x,int y)
{
cnt++;
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;++i)
{
int x,y;
scanf("%d%d",&x,&y);
add(x,y);
du[y]++;
}
queue q;
for(int i=1;i<=n;++i)
{
if(!du[i])
{
q.push(i);
dis[i]=1;
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(int i=head[u];i;i=nxt[i])
{
int v=to[i];
du[v]--;
dis[v]=dis[u]+1;
if(!du[v])
q.push(v);
}
}
for(int i=1;i<=n;++i)
printf("%d\n",dis[i]);
return 0;
}
比较神仙的一道就是球的序列,需要证明正向连边的错误以及反向连边的正确性。传送门
这种题有时会和贪心挂钩,由原图建成新的最小或者最大生成树,近年来好像也就考过货车运输一道,因为牵扯到树所以就可能有树上倍增或者差分甚至树链剖分这种东东,所以感觉要考也是放在T3这个位置(天天爱跑步就算了,大毒瘤)
次小生成树还得跟树剖这种挂钩。。。。。考的可能性不大
算法我一般都是用的kruskal(因为不会prim hhhh),利用并查集维护连通性即可。
首先可以想到边权小的边一定不会经过,所以只需要在原图的基础上构建一颗最大生成树即可,然后就是树上的一些操作。
#include
using namespace std;
const int MAXN=1e5+10;
const int MAXM=1e6+10;
const int INF=0x3f3f3f3f;
int n,m,q,cnt;
int head[MAXM],depth[MAXM],fa[MAXM];
int w[MAXN][21],p[MAXN][21];
struct Edge1{
int x,y,c;
friend inline bool operator<(const Edge1 &a,const Edge1 &b){
return a.c>b.c;
}
}t[MAXM];
struct Edge{
int from,to,w,nxt;
}edge[MAXM];
int Read(){
int i=0,f=1;
char c=getchar();
for(;(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-') f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar()) i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void add(int x,int y,int c){
++cnt;
edge[cnt].nxt=head[x];
edge[cnt].from=x;
edge[cnt].to=y;
edge[cnt].w=c;
head[x]=cnt;
}
int find(int x){
if(fa[x]!=x) fa[x]=find(fa[x]);
return fa[x];
}
void kruskal(){
int tot=0;
for(int i=1;i<=n;++i) fa[i]=i;
for(int i=1;i<=m;++i){
int fx=find(t[i].x),fy=find(t[i].y);
if(fx!=fy){
fa[fx]=fy;
add(t[i].x,t[i].y,t[i].c),add(t[i].y,t[i].x,t[i].c);
++tot;
if(tot==n-1) break;
}
}
}
void dfs(int u){
for(int i=head[u];i;i=edge[i].nxt){
int y=edge[i].to;
if(!depth[y]){
depth[y]=depth[u]+1;
p[y][0]=u;
w[y][0]=edge[i].w;
dfs(y);
}
}
}
void pre(){
for(int i=1;i<=n;++i){
if(!depth[i]){
depth[i]=1;
p[i][0]=0;
dfs(i);
}
}
dfs(1);
for(int i=1;i<=20;++i){
for(int x=1;x<=n;++x){
p[x][i]=p[p[x][i-1]][i-1];
w[x][i]=min(w[x][i-1],w[p[x][i-1]][i-1]);
}
}
}
int lca(int x,int y){
int ret=INF;
if(depth[x]=0;--i){
if(depth[p[x][i]]>=depth[y]){
ret=min(ret,w[x][i]);
x=p[x][i];
}
}
if(x==y) return ret;
for(int i=20;i>=0;--i){
if(p[x][i]!=p[y][i]){
ret=min(ret,min(w[x][i],w[y][i]));
x=p[x][i];
y=p[y][i];
}
}
ret=min(ret,min(w[x][0],w[y][0]));
return ret;
}
int main(){
n=Read(),m=Read();
for(int i=1;i<=m;++i) t[i].x=Read(),t[i].y=Read(),t[i].c=Read();
sort(t+1,t+m+1);
kruskal();
pre();
q=Read();
while(q--){
int dx=Read(),dy=Read();
if(find(dx)!=find(dy)) puts("-1");
else cout<
连通块的考法根据之前的NOIP模拟来看,考法很多样,割点割边,目前我最大的问题就是tarjan还不熟。。。。。卑微GLF在线自闭QAQ
这个东西还可以衍生出去,比如简单环最小环,不多赘述。
例题就是2018DAY2T1旅行,这道题后一半的部分分就是tarjan找环+枚举断边
典型例题受欢迎的牛。
本来不是NOIP考点。。。结果NOIP模拟还整个二分图,又整个网络流。。。。
重点还是在建图,还有就是神奇的二分图O(m sqrt(n))复杂度,考试的时候要敢想敢写。。。。
虽然NOIP就没考过这个。。。。
一直写的Dinic,没怎么看过ISAP。。。
例题就推荐一道KOS-DICING,一道二分+最大流的好题。
#include
using namespace std;
const int MAXN=2e4+10;
const int MAXM=1e5+10;
const int INF=0x3f3f3f3f;
int n,m,cnt,s,t,last;
int ans[MAXN];
int a[MAXN],b[MAXN];
int head[MAXN],cur[MAXN],depth[MAXN];
int nxt[MAXM],to[MAXM],w[MAXM];
int Read(){
int i=0,f=1;
char c;
for(c=getchar();(c>'9'||c<'0')&&c!='-';c=getchar());
if(c=='-')
f=-1,c=getchar();
for(;c>='0'&&c<='9';c=getchar())
i=(i<<3)+(i<<1)+c-'0';
return i*f;
}
void Add(int x,int y,int z){
nxt[cnt]=head[x];
head[x]=cnt;
to[cnt]=y;
w[cnt]=z;
cnt++;
}
void add(int x,int y,int z){
Add(x,y,z);
Add(y,x,0);
}
int bfs(){
queue q;
memset(depth,0,sizeof(depth));
depth[s]=1;
q.push(s);
while(!q.empty()){
int u=q.front();
q.pop();
for(int i=head[u];i!=-1;i=nxt[i]){
int v=to[i];
if(!depth[v]&&w[i]){
depth[v]=depth[u]+1;
q.push(v);
if(v==t)
return 1;
}
}
}
return 0;
}
int dfs(int u,int flow){
if(u==t)
return flow;
int res=0,di,v;
for(int &i=cur[u];i!=-1;i=nxt[i]){
v=to[i];
if(depth[v]==depth[u]+1&&w[i]){
di=dfs(v,min(w[i],flow));
if(di){
w[i]-=di;
w[i^1]+=di;
res+=di;
if(res==flow)
break;
}
}
}
return res;
}
int dinic(){
int ret=0;
while(bfs()){
memcpy(cur,head,sizeof(head));
ret+=dfs(s,INF);
}
return ret;
}
void build(int x){
cnt=0;
memset(head,-1,sizeof(head));
for(int i=1;i<=m;++i){
add(a[i],i+n,1);
add(b[i],i+n,1);
add(i+n,t,1);
}
for(int i=1;i<=n;++i)
add(s,i,x);
}
int check(int x){
build(x);
return dinic()==m;
}
void update(){
int v;
for(int u=1;u<=m;++u){
for(int i=head[u+n];i!=-1;i=nxt[i]){
v=to[i];
if(v<=n&&v!=s&&w[i]){
if(v==a[u])
ans[u]=1;
else
ans[u]=0;
}
}
}
}
int main(){
memset(head,-1,sizeof(head));
n=Read(),m=Read();
s=0,t=n+m+1;
for(int i=1;i<=m;++i){
a[i]=Read(),b[i]=Read();
}
int l=1,r=m,mid;
while(l<=r){
mid=l+r>>1;
if(check(mid))
last=mid,r=mid-1;
else
l=mid+1;
}
update();
cout<