2月1日 图论

终于来到了传说中的图论,第一天就被虐的死去活来,究其原因是因为曾经的一些东西如dp,bfs理解的不是很透,代码写不出来,尤其是递推这块非常不熟练,图论的有些东西就很不好,后续会继续在csdn上总结一下dp和bfs
今日主要内容:1、图的基本定义等(这个在网上都有,不多说了)
2、存图
3、图的遍历
4、算法——拓扑排序
从存图开始:
三种方法
1、邻接矩阵
用二维数组表示点和点之间是否有边,例如a[i][j] = w就表示从i到j有一条有向边,边权为w。
但是这个方法的空间复杂度是n^2,n稍微大一点就过不了了,所以不适用
2、不定长数组vector
这个就是开一个二维vector,每个点有一个vector,储存这一个点都连向哪些点
例如3->5的一条有向边:v[3].push_back(5);
这个的空间复杂度可以大大优化但是我们被告知这个方法可以编一些特殊的数据卡你,所以还是不太适用
3、链式向前星
这个主要是模拟链表
对于每一个节点:
对于每条边记录指向的点x,边权w,该边的编号,下一条边的编号nxt
为了存边的方便,我们对其进行了如下改进:nxt记录这条边前一条边编号,head[i]记录最后一个边的编号
这个比较复杂了(我个人感觉我还是喜欢vector,因为这个写起来麻烦而且不是那么好理解QAQ终究还是我太菜了啊)
那么这个上个代码:

struct Edge{
    int nxt,to,w;
}e[100001];
int a[1000001] = { };
int head[1000001],ecnt = 0;
void addEdge(int x,int y,int w){
    ++ecnt;
    e[ecnt].nxt = head[x];
    e[ecnt].to = y;
    e[ecnt].w = w;
    head[x] = ecnt;
}

这种存法的含义就是从x到y的权为w的边

还有一个简化之后的代码:

struct Edge{
    int nxt,to,w;
}e[100001];
int a[1000001] = { };
int head[1000001],ecnt = 0;
void addEdge(int x,int y,int w){
    e[++ecnt] = (Edge){head[x],y,w};
    head[x] = ecnt;
}

其实实现的东西都是一样的,就是短了点

那么接下来直接就写算法了:拓扑排序
将一个有向无环图所有节点转换成一个线性序列,是所有能走到v的点都排在v的前面
算法的思路:我们令du[i]表示入度,然后——
1、入度为0的,放入队列
2、取出队首u,把u到v所有边du[v] - 1
3、若du[v]=0,v出队
4、重复上述步骤
以下是代码:

void topo_sort(){
    queue<int>q;
    for(int i = 1;i <= n; i++){
        if(!du[i]) q.push(i);
        while(!q.empty()){
            int now = q.front();
            q.pop();
            for(int i = head[now];~i;i = e[i].nxt){
                int v = d[i].to;
                if(!--du[v])q.push(v);
            }
        }
    }
}

今天的题也是特别的令我懵逼啊
1、图的遍历
题目描述

给出N个点,M条边的有向图,对于每个点v,求A(v)表示从点v出发,能到达的编号最大的点。
输入格式
第1 行,2 个整数N,M。
接下来
M行,每行2个整数U i ,V i ,表示边(U i ,V i )。点用1,2,⋯,N编号。
输出格式
N 个整数
A(1),A(2),⋯,A(N)。
这道题可以说是一个模版题了,就是基本的图的遍历,可以考虑先把这道题记住

#include<cstdio>
#include<cstring>
using namespace std;
int n,m;
struct Edge{
    int nxt,to;
}e[100001];
int a[1000001] = { };
int head[1000001],ecnt = 0;
void addEdge(int x,int y){
    ++ecnt;
    e[ecnt].nxt = head[x];
    e[ecnt].to = y;
    head[x] = ecnt;
}
void dfs(int now,int p){
    a[now] = p;
    for(int i = head[now],v;~i; i = e[i].nxt){
        if(a[v = e[i].to]) continue;
        dfs(v,p);
    }
}
int main(){
    scanf("%d %d" ,&n,&m);
    memset(head,-1,sizeof(head));
    for(int i = 1;i <= m; i++){
        int x,y;
        scanf("%d %d" ,&x,&y);
        addEdge(y,x);
    }
    
    for(int i = n;i; i--){
        if(!a[i]) dfs(i,i);
    }
    for(int i = 1;i <= n; i++) printf("%d " ,a[i]);
    return 0;
}

2、奶牛吃饭
K(1≤K≤100)只奶牛分散在N(1≤N≤1000)个牧场.现在她们要集中起来进餐.牧场之间有M(1≤M≤10000)条有向路连接,而且不存在起点和终点相同的有向路.她们进餐的地点必须是所有奶牛都可到达的地方.那么,有多少这样的牧场呢?
这道题首先用一个桶,然后对有奶牛的点进行遍历,用dfs标记出他们能到达的点并把点加一,最后统计,如果点的数等于奶牛数,就说明是可以到达的

#include<cstdio>
#include<cstring>
using namespace std;
int k,n,m;
int pl[100001];
int jd[100001] = { };
int vis[100001] = { };
struct Edge{
    int nxt,to;
}e[100001];
int ecnt = 0;
int tot = 0;
int head[100001];
void addEdge(int x,int y){
    ++ecnt;
    e[ecnt].nxt = head[x];
    e[ecnt].to = y;
    head[x] = ecnt;
}
void dfs(int u){
    if(vis[u] == 1) return;
    vis[u] = 1;
    jd[u]++;
    for(int i = head[u];~i;i = e[i].nxt){
        int v = e[i].to;
        if(vis[v])continue;
        dfs(v);
    }
}
int main(){
    scanf("%d %d %d" ,&k,&n,&m);
    memset(head,-1,sizeof(head));
    for(int i = 1;i <= k; i++){
        scanf("%d" ,&pl[i]);
    }
    for(int i = 1;i <= m; i++){
        int a,b;
        scanf("%d %d" ,&a,&b);
        addEdge(a,b);
    }
    for(int i = 1;i <= k; i++){
        dfs(pl[i]);
        for(int i = 1;i <= n; i++){
            if(jd[i] >= k) tot++;
        }
        memset(vis,0,sizeof(vis));
    }
    printf("%d" ,tot);
    return 0;
}

3、杂物
John的农场在给奶牛挤奶前有很多杂务要完成,每一项杂务都需要一定的时间来完成它。比如:他们要将奶牛集合起来,将他们赶进牛棚,为奶牛清洗乳房以及一些其它工作。尽早将所有杂务完成是必要的,因为这样才有更多时间挤出更多的牛奶。当然,有些杂务必须在另一些杂务完成的情况下才能进行。比如:只有将奶牛赶进牛棚才能开始为它清洗乳房,还有在未给奶牛清洗乳房之前不能挤奶。我们把这些工作称为完成本项工作的准备工作。至少有一项杂务不要求有准备工作,这个可以最早着手完成的工作,标记为杂务1。John有需要完成的n个杂务的清单,并且这份清单是有一定顺序的,杂务k(k>1)的准备工作只可能在杂务1至k−1中。写一个程序从1到n读入每个杂务的工作说明。计算出所有杂务都被完成的最短时间。当然互相没有关系的杂务可以同时工作,并且,你可以假定John的农场有足够多的工人来同时完成任意多项任务。
把任务之间的关系抽象成一张图,会发现是一个DAG。
令dp[i]表示完成任务i需要的时间,则dp[u]=max{dp[v]}+a[u](<u,v>∈G).
在拓扑排序的过程中就能求出所有的dp[i],答案就是max{dp[i]}.
(这该死的dp,我一直就没学明白,这个题解来源于师兄的思路,等这一小波集训结束之后要赶紧想办法把dp弄明白了)

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
#include<string>
#include<iostream>
using namespace std;
int m,n,k;
int tim[10500];
int num=0;
int fi[10500]={ };
int a,b;
int ans[10500]={ };
struct node{
	int to,next;
}p[1000500];
int tot=0;
int jd[1500]={ };
void addline(int x,int y){
	p[++num]=(node){y,fi[x]};
	fi[x]=num;
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n;i++){
		scanf("%d%d",&a,&tim[i]);
		while(scanf("%d",&a)!=EOF){
			if(a==0) break;
			addline(i,a);
		}
	}
	for(int i=1;i<=n;i++){
		int mx=0;
		for(int j=fi[i];j;j=p[j].next){
			mx=max(mx,ans[p[j].to]);
		}
		ans[i]=mx+tim[i];
		tot=max(tot,ans[i]);
	}
	printf("%d",tot);
	return 0;
}

最大食物链
你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧。

题目描述

给你一个食物网,你要求出这个食物网中最大食物链的数量。

(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)

Delia 非常急,所以你只有
1
1 秒的时间。

由于这个结果可能过大,你只需要输出总数模上
80112002
80112002 的结果。
这道题也是一个递推,入度为0的一定在食物链末端,把这些点定为一,然后递推,下一个点就是指向它的所有点的大小的加和,以此类推

#include<cstdio>
#include<iostream>
#include<algorithm>
#include<queue>
using namespace std;
const int mod=80112002;
int n,m,cnt=0;
struct edge
{
    int to,nxt;
}edge[500005];
int head[500005],vis[500005],in[500005],out[500005],sum[500005];
void add(int x,int y)
{
    edge[++cnt].to=y;
    edge[cnt].nxt=head[x];
    head[x]=cnt;
}
int main()
{
    cin>>n>>m;
    int x,y;
    for(int i=1;i<=m;i++)
    {
        cin>>x>>y;
        add(x,y);
        in[y]++;
        out[x]++;
    }
    queue<int>q;
    for(int i=1;i<=n;i++)
    {
        if(!in[i])
        {
            q.push(i);
            sum[i]=1;
        }
    }
    int ans=0;
    while(!q.empty())
    {
        int v=q.front();
        q.pop();
        for(int i=head[v];i;i=edge[i].nxt)
        {
            int k=edge[i].to;
            sum[k]=(sum[k]+sum[v])%mod;
            in[k]--;
            if(in[k]==0)
            {
                q.push(k);
            }
        }
    }
    for(int i=1;i<=n;i++)
    {
        if(out[i]==0)
        {
            ans=(ans+sum[i])%mod;
        }
    }
    cout<<ans<<endl;
}

这道题是我写的非常不开心的一道题了啊,一开始审错题了,绕了不少弯路,然后有思路了因为dp这块没学好又写不出来代码,害

查找文献
小K 喜欢翻看洛谷博客获取知识。每篇文章可能会有若干个(也有可能没有)参考文献的链接指向别的博客文章。小K 求知欲旺盛,如果他看了某篇文章,那么他一定会去看这篇文章的参考文献(如果他之前已经看过这篇参考文献的话就不用再看它了)。
假设洛谷博客里面一共有 n(n≤10^5) 篇文章(编号为 1 到 n)以及 m(m≤10^6) 条参考文献引用关系。目前小 K 已经打开了编号为 1 的一篇文章,请帮助小 K 设计一种方法,使小 K 可以不重复、不遗漏的看完所有他能看到的文章。
这边是已经整理好的参考文献关系图,其中,文献 X → Y 表示文章 X 有参考文献 Y。不保证编号为 1 的文章没有被其他文章引用。
这道题就当作一个模版题先进行记忆吧,其中包括了vector的sort使用,还有图的dfs,bfs

#include<cstdio>
#include<iostream>
#include<queue>
#include<algorithm>
#include<cstring>
#include<vector>
using namespace std;
const int maxn=1e5+5;
vector<int> graph[maxn];
int flag[maxn];
void dfs(int v)
{
    flag[v]=true;
    cout<<v<<" ";
    for(int i=0;i<graph[v].size();i++)
    {
        if(!flag[graph[v][i]])
        {
            dfs(graph[v][i]);
        }
    }
}
void bfs(int v)
{
    queue<int>t;
    t.push(v);
    flag[v]=true;
    while(!t.empty())
    {
        int v1=t.front();
        t.pop();
        cout<<v1<<" ";
        for(int i=0;i<graph[v1].size();i++)
        {
            if(!flag[graph[v1][i]])
            {
                t.push(graph[v1][i]);
                flag[graph[v1][i]]=true;
            }
        }
    }
}
int main()
{
    int n,m,v1,v2;
    cin>>n>>m;
    for(int i=1;i<=m;i++)
    {
        cin>>v1>>v2;
        graph[v1].push_back(v2);
    }
    for(int i=1;i<=n;i++)
    {
        sort(graph[i].begin(),graph[i].end());
    }
    memset(flag,false,sizeof(flag));
    dfs(1);
    memset(flag,false,sizeof(flag));
    cout<<endl;
    bfs(1);
    return 0;
}

经过了这一天的自闭之旅,我发现我前面确实是有很多地方不扎实的,就比如说dp和bfs,这两个是目前我在学图论的时候因为不会对我影响最大的两个知识点,还有就是我带马能力确实不够,很多题思路出来了但是代码没法实现,这也是需要刷题训练的,最近赶紧烧时间恶补一下dp吧

上一篇:数据结构介绍——链式前向星


下一篇:2020寒假 Day-1