题解:洛谷 P11380 排队
【题目来源】
【题目描述】
小杨所在班级共有 \(n\) 位同学,依次以 \(1,2,\dots,n\) 标号。这 \(n\) 位同学想排成一行队伍,其中有些同学之间关系非常好,在队伍里需要排在相邻的位置。具体来说,有 \(m\) 对这样的关系(\(m\) 是一个非负整数)。当 \(m\geq 1\) 时,第 \(i\) 对关系(\(1\leq i\leq m\))给出 \(a_i,b_i\),表示排队时编号为 \(a_i\) 的同学需要排在编号为 \(b_i\) 的同学前面,并且两人在队伍中相邻。
现在小杨想知道总共有多少种排队方式。由于答案可能很大,你只需要求出答案对 \(10^9+7\) 取模的结果。
【输入】
第一行,两个整数 \(n,m\),分别表示同学们的数量与关系数量。
接下来 \(m\) 行,每行两个整数 \(a_i,b_i\),表示一对关系。
【输出】
一行,一个整数,表示答案对 \(10^9+7\) 取模的结果。
【输入样例】
4 2
1 3
2 4
【输出样例】
2
【核心思想】
-
问题分析:\(n\) 位同学排成一行,\(m\) 对关系要求 \(a_i\) 排在 \(b_i\) 前面且相邻。求合法排队方案数对 \(10^9+7\) 取模。本质是链表构建 + 排列组合问题,核心在于将相邻约束转化为链表结构,再计算链的排列数。
-
算法选择:
- 链表构建 + 环检测:用
l[]和r[]数组记录每个节点的左右邻居,构建若干条链 - 阶乘计算排列数:若有 \(cnt\) 条互不干扰的链,则方案数为 \(cnt!\)
- 链表构建 + 环检测:用
-
关键步骤:
- 读入数据:\(n\)、\(m\),以及 \(m\) 对关系 \((a_i, b_i)\)
- 构建链表并检测矛盾:
- 若 \(l[b] \neq 0\) 且 \(l[b] \neq a\)(\(b\) 左边已有其他人),或 \(r[a] \neq 0\) 且 \(r[a] \neq b\)(\(a\) 右边已有其他人),输出 \(0\)
- 否则:\(l[b] = a\),\(r[a] = b\)
- DFS 遍历链并检测环:
- 对每个链头(\(l[i] = 0\))开始 DFS,统计访问节点数 \(num\)
- 若访问到已访问节点,说明有环,输出 \(0\)
- 验证完整性:若 \(num \neq n\),说明有孤立节点无法排入链,输出 \(0\)
- 计算答案:\(ans = cnt! \bmod (10^9+7)\),其中 \(cnt\) 为链的数量
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),构建链表 \(O(m)\),DFS 遍历 \(O(n)\),阶乘计算 \(O(cnt) \leq O(n)\)
- 空间复杂度:\(O(n)\),链表数组和访问标记
-
链表构建与排列组合的核心思想:
- 相邻约束的链表化:每对关系 \(a \to b\) 表示 \(a\) 和 \(b\) 必须相邻且 \(a\) 在左。多个关系可能连接成链(如 \(1 \to 3 \to 5\)),链内顺序固定,链间可任意排列
- 矛盾检测的完备性:一个节点最多有一个左邻居和一个右邻居,违反此约束即存在矛盾(如 \(a \to b\) 且 \(c \to b\))。同时检测环(如 \(1 \to 2 \to 1\))和孤立节点
- 链的排列独立性:每条链作为一个整体参与排列,\(cnt\) 条链的全排列数为 \(cnt!\)。链内部顺序已由约束唯一确定,无需额外计算
- DFS 的环检测:从链头出发沿
r[]指针遍历,若遇到已访问节点则说明存在环(矛盾关系),立即输出 \(0\) - 适用于相邻约束排列、链表构建、环检测类问题
【算法标签】
普及 #排列组合
【代码详解】
#include <bits/stdc++.h>
using namespace std;
#define int long long // 将int宏定义为long long,防止阶乘计算溢出
const int N = 200005, mod = 1e9+7; // N为最大节点数常量,mod为取模数1e9+7
int n, m, num; // n为同学数量,m为关系数量,num为DFS已访问的节点数
int l[N], r[N]; // l[u]表示u左边的同学编号,r[u]表示u右边的同学编号(构建链表关系)
bool vis[N]; // vis[u]标记节点u是否已被访问(用于检测环)
void dfs(int u)
// 深度优先搜索,从链头u开始遍历整条链,统计链长度并检测是否有环
{
if (vis[u]) // 如果u已经被访问过,说明存在环(矛盾关系)
{
cout << 0 << endl; // 输出0表示不存在合法排队方案
exit(0); // 直接退出程序
}
num++; // 已访问节点数加1
vis[u] = 1; // 标记u已访问
if (r[u]!=0) // 如果u有右边的相邻同学
dfs(r[u]); // 递归访问右边的同学,继续遍历链条
}
signed main() // 使用signed main是因为#define int long long后,main返回值类型需要显式声明
{
cin >> n >> m; // 读入同学数量n和关系数量m
for (int i=1; i<=m; i++) // 循环读入m对相邻关系
{
int a, b; cin >> a >> b; // 读入一对关系:a需要排在b前面且相邻(a在b左边)
// 检查关系是否矛盾:
// 1. b已经有左边的人l[b],且不是a(b左边只能有一个人)
// 2. a已经有右边的人r[a],且不是b(a右边只能有一个人)
if (l[b]!=0 && l[b]!=a || r[a]!=0 && r[a]!=b)
{
cout << 0 << endl; // 关系矛盾,不存在合法方案
return 0; // 直接结束程序
}
l[b] = a; // 记录b的左边是a
r[a] = b; // 记录a的右边是b
}
int cnt = 0; // cnt记录链条的数量(每个链头对应一条链)
for (int i=1; i<=n; i++) // 遍历所有同学,寻找链头(左边没有人的同学)
{
if (l[i]==0) // 如果i的左边没有人,说明i是一条链的链头
{
cnt++; // 链条数量加1
dfs(i); // 从链头i开始DFS遍历整条链
}
}
if (num!=n) // 如果DFS访问的节点数不等于n,说明有节点不在任何链中(孤立点或矛盾)
{
cout << 0 << endl; // 存在无法排入链的节点,不存在合法方案
return 0; // 直接结束程序
}
int ans = 1; // ans为最终答案,初始化为1
for (int i=1; i<=cnt; i++) // 计算cnt条链的全排列数
ans = ans * i % mod; // ans = cnt! % mod,即cnt条链可以有cnt!种排列方式
cout << ans << endl; // 输出合法排队方案数对1e9+7取模的结果
return 0;
}
【运行结果】
4 2
1 3
2 4
2
浙公网安备 33010602011771号