2021-12-9 关于校赛的复盘
1.最大食物链计数
题目背景
你知道食物链吗?Delia 生物考试的时候,数食物链条数的题目全都错了,因为她总是重复数了几条或漏掉了几条。于是她来就来求助你,然而你也不会啊!写一个程序来帮帮她吧。
题目描述
给你一个食物网,你要求出这个食物网中最大食物链的数量。
(这里的“最大食物链”,指的是生物学意义上的食物链,即最左端是不会捕食其他生物的生产者,最右端是不会被其他生物捕食的消费者。)
Delia 非常急,所以你只有 11 秒的时间。
由于这个结果可能过大,你只需要输出总数模上 8011200280112002 的结果。
输入格式
第一行,两个正整数 n、mn、m,表示生物种类 nn 和吃与被吃的关系数 mm。
接下来 mm 行,每行两个正整数,表示被吃的生物A和吃A的生物B。
输出格式
一行一个整数,为最大食物链数量模上 8011200280112002 的结果。
输入样例
5 7
1 2
1 3
2 3
3 5
2 5
4 5
3 4
输出样例
5
说明/提示
各测试点满足以下约定:
【补充说明】
数据中不会出现环,满足生物学的要求。
问题分析
很明显,题目所说的生物链很契合无环有向图(DAG)图的特征,我们最终的任务是统计生物链个数,就可以转换成统计DAG图的路径条数问题,接下来解这道题的话,思考三个问题:1.选用什么存储结构表达DAG图,2.从入度为0的点到出度为0的节点的极长链怎么通过图像来表示出来,3.最后的路径数该怎么算,取模怎么取??
第一、图像的储存结构,我们熟知的有两种,一种是邻接矩阵,另外一种是邻接表。选用邻接矩阵或许不失为一种简单方法,但是很多空间会浪费。邻接表在空间上能更优化,但是实现比较麻烦。还有一种是链式前向星,思路上和邻接表很相似,但是可能难理解一点,后续需要研究一波。
第二、此题题目上说前面没有生物可以吃它,后面他没有生物可以吃,前面入度为0,后面出度为0,这种特征选用拓扑排序,拓扑排序就是通过入度为0的顶点来将一个网构造出“拓扑有序序列”,与题目中的一条生物链很相似,所以可以尝试这种方法。
我的大致思路:
1.先初始化:将入度为0的顶点放入队列当中
2.然后陆续出列,删去这个顶点的弧,具体代码实现也很简单,比如u->v的边,将u的出度减1,v的入度减1
3.然后判断,要是这个顶点的入度为0,就把他入队,等待后续出队操作(删去弧边),另外注意一下,如果他出度为0了,那么这个顶点i的dp[i]就是以i为终点的所有路径条数决策数。后续所有的路径数就累加即可(所有出度为0的点为终点的顶点的路径和)
4.就这样一直循环,直到最后队列为空为止(此时所有顶点都出来了,因为没有回路,就不存在没有前驱的节点)
第三、路径条数怎么算?这里就是在拓扑排序的过程中用DP就可以了,状态转移方程是dp[v]=dp[v]+dp[u],注意加完以后取模就行了,因为最后的值很大,所以要注意按照题目要求加完以后取模。
二维数组构造邻接矩阵代码
一些说明:
本来是用int存储邻接矩阵的,但是因为此题没有权值,所以我们只需要知道他是否连通,所以我们可以用char,short或是bool类型在一定程度上减少空间的占用,从而优化代码。
删除这条边就是让边不连通即可,比如这里我是令其为false
a[x][y] = false;
参考代码
#include <stdio.h>
#include <queue>
using namespace std;
#define maxn 5005
const int mod = 80112002;
bool a[maxn][maxn]; //邻接矩阵
int in[maxn], out[maxn]; //顶点的入度,出度
int dp[maxn], n, m, ans; //到达此顶点的路径数,顶点数,边数,所有路径数之和
queue<int> q;
//输入,并设定好相应入度出度以及邻接矩阵,并将入度为0的放入队列,等待后续处理
void init()
{
scanf("%d%d", &n, &m);
int x, y;
for (int i = 0; i < m; i++)
{
scanf("%d%d", &x, &y);
a[x][y] = true;
out[x]++;
in[y]++;
}
for (int i = 1; i <= n; i++)
{
if (in[i] == 0)
{
dp[i] = 1;
q.push(i);
}
}
}
void slove()
{
while (!q.empty())
{
int front = q.front();
q.pop();
for (int i = 1; i <= n; i++)
//寻找与front相连的顶点,做后续操作
{
if (!a[front][i])
continue;
a[front][i] = 0;
dp[i] = dp[i] + dp[front];
dp[i] %= mod;
out[front]--;
in[i]--;
if (in[i] == 0)
{
if (out[i] == 0)
{
ans += dp[i];
ans %= mod;
continue;
}
q.push(i);
}
}
}
printf("%d\n", ans);
}
int main()
{
init();
slove();
return 0;
}
构造前向星的代码
(可参考此网站)
我的一些理解
结构体定义(注:这是C++写法,没有typedef时,后面直接跟上结构变量)参考网站戳这
struct edge
{
int to, weight, next;
//to是边的终点,weight是边的权重,next是以边的起点u时的上一条边的编号
} edge[maxn];
//我所输入的每一条边的序号信息存入此结构体数组中
head[ i ]数组是以i为起点的最后一条边序号
我们是通过head [ i ]不断通过next(同样以i为起点的上一条边)来访问起点为i的边,用edge[i].to表示这条边的终点,最后head[i]=-1时结束访问,这是不是和邻接表的链式结构很相似呢?
最后附上我的参考代码
#include <stdio.h>
#include <queue>
#define maxn 5000005
using namespace std;
const int mod = 80112002;
struct edge
{
int to, weight, next;
} edge[maxn];
queue<int> q;
int in[maxn], out[maxn], dp[maxn], head[maxn];
int m, n, cnt, ans; // cnt动态记录边的个数.ans记录结果
//初始化:以 i 为起点的最后一条边的编号初始化为-1。(后续前向星的终止条件)
void init()
{
for (int i = 0; i < m; i++)
head[i] = -1;
cnt = 0;
}
void add_edge(int u, int v)
{
edge[cnt].to = v;
edge[cnt].next = head[u]; //第cnt+1条边的上一条边是以u为起点的最后一条边
head[u] = cnt++;
}
void slove()
{
while (!q.empty())
{
int front = q.front();
q.pop();
int i;
for (i = head[front]; i != -1; i = edge[i].next)
{
dp[edge[i].to] = dp[edge[i].to] + dp[front];
dp[edge[i].to] %= mod;
out[front]--;
in[edge[i].to]--;
if (in[edge[i].to] == 0)
{
if (out[edge[i].to] == 0)
{
ans += dp[edge[i].to];
ans %= mod;
continue;
}
q.push(edge[i].to);
}
}
}
printf("%d\n", ans);
}
int main()
{
int u, v;
//输入
scanf("%d%d", &n, &m);
//前向星构造
init();
for (int i = 0; i < m; i++)
{
scanf("%d%d", &u, &v);
add_edge(u, v);
out[u]++;
in[v]++;
}
//拓扑排序,所有入度为0的点放入队列等待处理,并设置到此节点的决策数为1种
for (int i = 1; i <= n; i++)
{
if (in[i] == 0)
{
dp[i] = 1;
q.push(i);
}
}
slove();
return 0;
}
ps:小白不才,最后欢迎大佬指正