【问题描述】
给定一组 n 人(编号为 1, 2, ..., n), 我们想把每个人分进任意大小的两组。每个人都可能不喜欢其他人,那么他们不应该属于同一组。
给定整数 n 和数组 dislikes ,其中 dislikes[i] = [ai, bi] ,表示不允许将编号为 ai 和 bi的人归入同一组。当可以用这种方法将所有人分进两组时,返回 true;否则返回 false。
说明:
1 <= n <= 2000
0 <= dislikes.length <= 104
dislikes[i].length == 2
1 <= dislikes[i][j] <= n
ai < bi
dislikes 中每一组都 不同
来源:力扣(LeetCode)
链接:https://leetcode.cn/problems/possible-bipartition/
【算法设计】
请设计算法,求解上述问题。用结构化程序设计方法编程验证算法的正确性。
输入格式:
每行输入的第一个数n(1 <= n <= 2000 )为题目描述中的数组dislikes长度,n=0的时候输入结束;接下来若干组不喜欢的数据,每2个整数一组,之间用空格隔开,组间使用逗号分割,所有不喜欢数据结束后使用分号隔开。
输出格式:
每行输出为一个输出true或者false,表示是否可以将所有人分进两组。
样例输入:
4 1 2,1 3,2 4;
0
样例输出:
true
解释:group1 [1,4], group2 [2,3]
输入处理:
从输入中读取整数 n,表示人的数量。
如果 n 为 0,则输入结束。
读取一组不喜欢的数据,每组由两个整数表示,使用逗号分隔,存储到数组中。
重复上述步骤,直到遇到分号表示所有不喜欢的数据输入结束。
数据结构设计:
创建一个邻接表或邻接矩阵来表示人与不喜欢的人之间的关系。
算法设计:
使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历每个人,并为其分配一个组别(例如1和2)。
在遍历的过程中,检查每个人的不喜欢列表,如果不喜欢的人已经被分配到了同一组,则返回 false。
如果遍历完成后没有发现冲突,返回 true。
输出结果:
根据算法的返回结果,输出 true 或 false,表示是否可以将所有人分进两组。
根据以上任务分析,可以开始实现算法验证的程序编写。
深度优先搜索
深度优先搜索(Depth-First Search,DFS)是一种遍历或搜索图或树的算法,其基本思想是尽可能深地探索每个分支,直到达到最深处,然后再回溯到前一个节点继续探索其他分支。在解决可能的二分法问题时,可以使用DFS来判断是否可以将所有人分进两组。
以下是使用DFS解决可能的二分法问题的算法步骤:
创建一个邻接表或邻接矩阵来表示人与不喜欢的人之间的关系。
创建一个数组 groups,用于记录每个人所属的组别,初始值为0。
对于每个未被分组的人,进行DFS遍历:
如果当前人已经被分组,跳过。
设置当前人的组别为1。
对当前人的不喜欢列表中的每个人进行递归DFS遍历:
如果该人已经被分组,检查其组别是否与当前人的组别相同,如果相同则返回 false。
如果该人未被分组,将其组别设置为与当前人不同的组别,并进行递归DFS遍历。
如果DFS遍历结束后没有发现冲突(即未出现同组不喜欢的情况),返回 true;否则返回 false。
算法的复杂度分析:
- 时间复杂度:在最坏情况下,需要遍历所有的人和他们的不喜欢列表,因此时间复杂度为 O(n + m),其中 n 是人的数量,m 是不喜欢关系的数量。
- 空间复杂度:需要使用额外的空间来存储邻接表或邻接矩阵、groups 数组以及递归调用的栈空间,因此空间复杂度为 O(n + m)。
广度优先搜索
广度优先搜索(Breadth-First Search,BFS)是一种遍历或搜索图或树的算法,其基本思想是逐层地扩展搜索,先访问当前节点的所有邻居节点,然后再逐层访问下一层的节点。在解决可能的二分法问题时,也可以使用BFS来判断是否可以将所有人分进两组。
以下是使用BFS解决可能的二分法问题的算法步骤:
创建一个邻接表或邻接矩阵来表示人与不喜欢的人之间的关系。
创建一个数组 groups,用于记录每个人所属的组别,初始值为0。
创建一个队列 queue,用于存储待访问的人。
对于每个未被分组的人,进行BFS遍历:
将当前人入队。
设置当前人的组别为1。
循环遍历队列中的人:
出队一个人,记为 person。
对 person 的不喜欢列表中的每个人进行处理:
如果该人已经被分组,检查其组别是否与 person 的组别相同,如果相同则返回 false。
如果该人未被分组,将其组别设置为与 person 不同的组别,并将其入队。
如果BFS遍历结束后没有发现冲突(即未出现同组不喜欢的情况),返回 true;否则返回 false。
算法的复杂度分析:
- 时间复杂度:在最坏情况下,需要遍历所有的人和他们的不喜欢列表,因此时间复杂度为 O(n + m),其中 n 是人的数量,m 是不喜欢关系的数量。
- 空间复杂度:需要使用额外的空间来存储邻接表或邻接矩阵、groups 数组以及队列的空间,因此空间复杂度为 O(n + m)。
程序设计与实现
深度优先搜索代码的模块划分:
对于深度优先搜索算法的实现,可以将其划分为以下几个模块:
- 主函数模块:
- 读取输入,包括人的数量和不喜欢的数据。
- 调用深度优先搜索算法函数,并输出结果。
- 深度优先搜索函数模块:
- 输入参数:当前人的编号、当前人所属的组别、邻接表或邻接矩阵、已分配组别的数组。
- 递归终止条件:如果当前人已经被分组,返回 true。
- 分配当前人的组别,并将其标记为已分组。
- 遍历当前人的不喜欢列表:
- 如果不喜欢的人已经被分组,检查其组别是否与当前人的组别相同,如果相同则返回 false。
- 如果不喜欢的人未被分组,递归调用深度优先搜索函数,并将其分配到与当前人不同的组别。
- 返回 true。
- 邻接表/邻接矩阵构建模块:
- 输入参数:不喜欢的数据。
- 根据不喜欢的数据构建邻接表或邻接矩阵,表示人与不喜欢的人之间的关系。
- 辅助函数模块:
- 输入参数:不喜欢的数据。
- 解析不喜欢的数据,提取出每对不喜欢的人的关系。
以上模块的划分可以根据具体的编程语言和代码实现方式进行调整。根据模块划分,可以开始实现深度优先搜索算法的程序代码。
广度优先搜索代码的模块划分:
对于广度优先搜索算法的实现,可以将其划分为以下几个模块:
- 主函数模块:
- 读取输入,包括人的数量和不喜欢的数据。
- 调用广度优先搜索算法函数,并输出结果。
- 广度优先搜索函数模块:
- 输入参数:邻接表或邻接矩阵、已分配组别的数组。
- 创建一个队列,并将起始人入队。
- 循环遍历队列,直到队列为空:
- 出队一个人。
- 遍历该人的不喜欢列表:
- 如果不喜欢的人已经被分组,检查其组别是否与当前人的组别相同,如果相同则返回 false。
- 如果不喜欢的人未被分组,将其分配到与当前人不同的组别,并将其入队。
- 返回 true。
- 邻接表/邻接矩阵构建模块:
- 输入参数:不喜欢的数据。
- 根据不喜欢的数据构建邻接表或邻接矩阵,表示人与不喜欢的人之间的关系。
- 辅助函数模块:
- 输入参数:不喜欢的数据。
- 解析不喜欢的数据,提取出每对不喜欢的人的关系。
以上模块的划分可以根据具体的编程语言和代码实现方式进行调整。根据模块划分,可以开始实现广度优先搜索算法的程序代码。
2.4程序运行、测试与分析
2.4.1深度优先搜索法测试结果
图2-1 力扣测试图
本地测试如图2-2所示:
图2-1 本地测试图
DFS算法的测试结果分析:
1. 正确性:经过测试,该算法得到了样例输入的正确输出结果,因此可以判断该算法是正确的。
2. 时间复杂度:在最坏情况下,需要遍历所有的人和他们的不喜欢列表,因此时间复杂度为 O(n + m),其中 n 是人的数量,m 是不喜欢关系的数量。
- 需要使用额外的空间来存储邻接表或邻接矩阵、groups 数组以及递归调用的栈空间,因此空间复杂度为 O(n + m)。
2.4.2 广度优先搜索测试结果
广度优先搜索算法的测试结果分析:
1. 正确性:经过测试,该算法得到了样例输入的正确输出结果,因此可以判断该算法是正确的。
2. 时间复杂度:在最坏情况下,需要遍历所有的人和他们的不喜欢列表,因此时间复杂度为 O(n + m),其中 n 是人的数量,m 是不喜欢关系的数量。
3.空间复杂度:需要使用额外的空间来存储邻接表或邻接矩阵、groups 数组以及队列的空间,因此空间复杂度为 O(n + m)。
2.4.3 算法性能差异分析
深度优先搜索(DFS)和广度优先搜索(BFS)是两种常用的图搜索算法,它们在性能和应用场景上有一些差异。
性能差异:
- 时间复杂度:在相同的图结构下,DFS和BFS的时间复杂度都是O(V + E),其中V是图中顶点的数量,E是边的数量。但是DFS和BFS的时间复杂度实际上受到搜索路径的长度影响,因此在不同的图结构下,实际的执行时间可能有所差异。
- 空间复杂度:DFS的空间复杂度主要取决于递归调用栈的深度,而BFS的空间复杂度取决于队列中存储的顶点数量。在最坏情况下,DFS的空间复杂度是O(V),而BFS的空间复杂度是O(V),因为BFS需要维护一个队列来存储顶点。
应用场景差异:
- DFS适用于解决路径搜索问题,例如图的连通性、拓扑排序、深度限制搜索等。DFS通过深度优先的方式遍历图,可以快速到达图的深层节点,但可能无法找到最短路径。
- BFS适用于解决最短路径问题,例如图的最短路径、层级遍历等。BFS通过广度优先的方式遍历图,可以找到最短路径,但相对而言可能需要更多的内存空间。
总结:
- DFS适合解决路径搜索问题,通过深度优先的方式遍历图,空间利用率高,但可能无法找到最短路径。
- BFS适合解决最短路径问题,通过广度优先的方式遍历图,可以找到最短路径,但需要更多的内存空间。
在选择使用DFS还是BFS时,需要根据具体的问题要求和图结构特点来进行选择。
本题的设计任务与目标包括:
本题的设计任务与目标是判断是否可以将给定的n个人分成两组,使得每组中的人互相不喜欢。你希望设计一个算法来解决这个问题。
可以使用图的着色问题来解决这个任务。首先,根据不喜欢的关系构建一个图,其中每个人表示一个节点,不喜欢的关系表示节点之间的边。然后,使用深度优先搜索(DFS)或广度优先搜索(BFS)遍历图,并给每个节点着色,使得相邻节点的颜色不同。
如果在遍历过程中发现某个节点的相邻节点已经着色,并且颜色相同,则说明无法将这些人分成两组,返回false。如果遍历完成后没有发现相邻节点着色相同的情况,则说明可以将这些人分成两组,返回true。
3.1.1 广度优先设计任务与目标
1. 广度优先搜索设计的任务与目标包括:
- 寻找最短路径:BFS可以用于寻找图中两个节点之间的最短路径。通过从起始节点开始,逐层访问其相邻节点,并记录路径长度,直到找到目标节点或遍历完所有可能的路径。
- 无权图的最短路径:当图的边没有权重时,BFS可以用于寻找无权图中节点之间的最短路径。由于BFS沿着图的层级进行遍历,所以首次到达目标节点的路径长度就是最短路径长度。
- 图的连通性检测:BFS可以用于检测图是否是连通的,即是否存在一条路径可以从一个节点到达另一个节点。通过从任意一个节点开始进行BFS遍历,如果遍历到的节点数量等于图中的总节点数量,则说明图是连通的。
- 层级遍历:BFS按照层级的方式遍历图,可以用于获取图中每个节点所在的层级信息。这在一些问题中非常有用,比如查找社交网络中的朋友关系,或者在树结构中查找某一层级的节点。
- 集合分割:BFS可以用于将图中的节点划分为不相交的集合。通过从每个未访问的节点开始进行BFS遍历,并将遍历过的节点加入到一个集合中,可以将节点划分为多个不相交的集合。
3.1.2深度优先搜索设计任务与目标
深度优先搜索设计的任务与目标包括:
- 寻找路径或遍历图:DFS可以用于寻找图中两个节点之间的一条路径,或者遍历整个图的所有节点。通过从起始节点开始,深度优先地探索每个可能的路径,直到找到目标节点或遍历完整个图。
- 拓扑排序:DFS可以用于对有向无环图(DAG)进行拓扑排序。拓扑排序是对图中所有节点进行排序,使得对于每条有向边 (u, v),节点 u 在排序中位于节点 v 的前面。通过DFS遍历图,并记录节点的访问顺序,可以得到拓扑排序结果。
- 判断图中的环:DFS可以用于判断无向图或有向图中是否存在环。在DFS的过程中,如果遇到已经访问过的节点,并且该节点不是当前节点的父节点,那么说明存在环。
- 连通性分析:DFS可以用于分析图的连通性。通过从一个节点开始进行DFS遍历,可以确定从该节点可达的所有其他节点,从而得到图的连通分量。
- 状态空间搜索:DFS可以用于在状态空间中搜索特定目标状态。状态空间搜索是一种在多个状态之间进行转移的过程,通过DFS遍历所有可能的状态转移路径,直到达到目标状态或遍历完整个状态空间。
系统设计是指在解决特定问题或实现特定功能时,从整体架构和模块化的角度出发,设计系统的各个组成部分之间的关系、功能和交互方式。系统设计的任务与目标包括:
任务:
需求分析:深入理解用户需求,明确系统的功能和性能要求。
架构设计:设计系统的整体结构,包括模块划分、组件之间的交互方式等。
模块设计:设计各个模块的功能和接口,确保模块之间的协作和数据流畅。
数据设计:设计系统中的数据结构、存储方式和数据处理流程。
安全设计:考虑系统的安全性,包括数据保护、用户认证等方面。
性能设计:考虑系统的性能优化,包括响应时间、吞吐量等。
扩展性设计:考虑系统的可扩展性,确保系统能够应对未来的需求变化和扩展。
界面设计:设计用户界面,确保用户友好性和易用性。
目标:
功能完备:确保系统实现了用户需求的所有功能。
性能优化:设计高效的系统架构和算法,保证系统具有良好的性能。
安全可靠:确保系统的数据安全性和稳定性。
易维护:设计清晰的模块化结构,便于系统的维护和升级。
用户体验:设计友好的用户界面,提升用户体验和用户满意度。
通过系统设计,可以建立一个符合需求、高效可靠、易维护的系统,满足用户需求并提供良好的用户体验。系统设计是软件开发过程中至关重要的一环,能够确保项目顺利实施并达到预期目标。
3.2.1 广度优先搜索法系统设计
系统设计:
o 类设计:BipartiteGraph
o 算法设计:使用广度优先搜索(BFS)和颜色标记法来判断是否可以将人分进两组。
o 输入输出设计:使用输入参数 n 和 dislikes,输出布尔值表示是否可以将人分进两组。
功能:
o 判断是否可以将一组人分进两组,使得不喜欢彼此的人不在同一组。
数据结构:
o 邻接表(adjacency list):用于表示人与其不喜欢的人之间的关系。
o 颜色数组(color array):用于标记每个人所属的组别,1 和 -1 分别表示两个不同的组。
算法复杂度:
o 时间复杂度:O(n + m),其中 n 是人的数量,m是不喜欢关系的数量。
o 空间复杂度:O(n+m),存储邻接表和颜色数组所需的空间。。
3.2.2 DFS系统设计
系统设计:
- 类设计:GraphDFS
- 算法设计:使用深度优先搜索(DFS)算法来判断是否可以将人分进两组。
- 输入输出设计:使用输入参数 n 和 dislikes,输出布尔值表示是否可以将人分进两组。
功能: - 判断是否可以将一组人分进两组,使得不喜欢彼此的人不在同一组。
数据结构: - 邻接表(adjacency list):用于表示人与其不喜欢的人之间的关系。
- 颜色数组(color array):用于标记每个人所属的组别,1 和 -1 分别表示两个不同的组。
算法复杂度: - 时间复杂度:O(n + m),其中 n 是人的数量,m 是不喜欢关系的数量。
- 空间复杂度:O(n+m),存储邻接表和颜色数组所需的空间。
3.3.1 BFS系统实现
这段代码实现了使用广度优先搜索(BFS)来判断是否可以将一组人分成两组,使得不喜欢彼此的人不在同一组。
- possibleBipartition 函数是整个算法的入口。它接受参数 n 表示人的数量,dislikes 是一个二维向量,表示不喜欢关系的数组。
- 首先,创建一个大小为 n+1 的颜色数组 color,用于标记每个人所属的组别。初始时,所有人的颜色都为0。
- 创建一个邻接表 g,用于存储每个人与其不喜欢的人之间的关系。遍历 dislikes 数组,将每对不喜欢关系添加到邻接表中。
- 接下来,使用一个循环遍历所有人,对于每个人执行以下操作:
- 如果该人的颜色为0,表示还没有被分组,则将其加入队列 q,并将其颜色设置为1。
- 进入一个内部的循环,直到队列为空。在每次循环中,取出队列的头部元素 t。
- 遍历 t 不喜欢的人的列表 g[t],对于每个不喜欢的人 next 执行以下操作:
- 如果 next 已经被分组,并且与 t 的颜色相同,说明无法满足要求,返回 false。
- 如果 next 还没有被分组,则将其颜色设置为 3 ^ color[t],其中 ^ 表示异或操作,表示将 color[t] 的值取反,然后将 next 加入队列 q。
- 循环结束后,表示所有与当前人 i 相关的人都被正确分组,继续遍历下一个人。
- 如果所有人都被正确分组,没有出现矛盾的不喜欢关系,返回 true;否则返回 false。
该算法使用了广度优先搜索来遍历图,通过颜色标记法判断不喜欢彼此的人是否可以分在不同的组。时间复杂度为 O(n + dislikes.length),空间复杂度为 O(n)。
源代码如下:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
class Solution {
public:
bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
vector<int> color(n + 1, 0);
vector<vector<int>> g(n + 1);
for (auto& p : dislikes) {
g[p[0]].push_back(p[1]);
g[p[1]].push_back(p[0]);
}
for (int i = 1; i <= n; ++i) {
if (color[i] == 0) {
queue<int> q;
q.push(i);
color[i] = 1;
while (!q.empty()) {
auto t = q.front();
q.pop();
for (auto& next : g[t]) {
if (color[next] > 0 && color[next] == color[t]) {
return false;
}
if (color[next] == 0) {
color[next] = 3 ^ color[t];
q.push(next);
}
}
}
}
}
return true;
}
};
int main() {
int n;
cout << "Enter the number of people: ";
cin >> n;
vector<vector<int>> dislikes;
int m;
cout << "Enter the number of dislike pairs: ";
cin >> m;
cout << "Enter the dislike pairs (e.g., 1 2): " << endl;
for (int i = 0; i < m; ++i) {
int person1, person2;
cin >> person1 >> person2;
dislikes.push_back({ person1, person2 });
}
Solution solution;
bool isPossible = solution.possibleBipartition(n, dislikes);
if (isPossible) {
cout << "true." << endl;
}
else {
cout << "false." << endl;
}
return 0;
}
3.3.2 DFS算法系统实现
这段代码实现了使用深度优先搜索(DFS)来判断是否可以将一组人分成两组,使得不喜欢彼此的人不在同一组。
- dfs 函数是递归函数,用于对当前节点进行深度优先搜索。它接受参数 curnode 表示当前节点,nowcolor 表示当前节点的颜色,color 是颜色数组,g 是邻接表。
- 在每次递归调用开始时,将当前节点的颜色设置为 nowcolor,表示将当前节点分为 nowcolor 组。
- 遍历当前节点 curnode 的相邻节点 nextnode,对于每个相邻节点执行以下操作:
- 如果相邻节点 nextnode 已经被分组,并且与当前节点 curnode 的颜色相同,说明无法满足要求,返回 false。
- 如果相邻节点 nextnode 还没有被分组,则递归调用 dfs 函数,将相邻节点 nextnode 的颜色设置为 3 ^ nowcolor,其中 ^ 表示异或操作,表示将 nowcolor 的值取反,继续深度优先搜索。
- 如果递归调用返回 false,说明出现矛盾,不满足要求,返回 false。
- 在 possibleBipartition 函数中,创建一个大小为 n+1 的颜色数组 color,用于标记每个人所属的组别。初始时,所有人的颜色都为0。
- 创建一个邻接表 g,用于存储每个人与其不喜欢的人之间的关系。遍历 dislikes 数组,将每对不喜欢关系添加到邻接表中。
- 使用一个循环遍历所有人,对于每个人执行以下操作:
- 如果该人的颜色为0,表示还没有被分组,则调用 dfs 函数对该人进行深度优先搜索。
- 如果 dfs 函数返回 false,说明出现矛盾,不满足要求,返回 false。
- 如果所有人都被正确分组,没有出现矛盾的不喜欢关系,返回 true;否则返回 false。
该算法使用了深度优先搜索来遍历图,通过颜色标记法判断不喜欢彼此的人是否可以分在不同的组。时间复杂度为 O(n + dislikes.length),空间复杂度为 O(n)。
源代码如下:
#include <iostream>
#include <vector>
using namespace std;
class Solution {
public:
bool dfs(int curnode, int nowcolor, vector<int>& color, const vector<vector<int>>& g) {
color[curnode] = nowcolor;
for (auto& nextnode : g[curnode]) {
if (color[nextnode] && color[nextnode] == color[curnode]) {
return false;
}
if (!color[nextnode] && !dfs(nextnode, 3 ^ nowcolor, color, g)) {
return false;
}
}
return true;
}
bool possibleBipartition(int n, vector<vector<int>>& dislikes) {
vector<int> color(n + 1, 0);
vector<vector<int>> g(n + 1);
for (auto& p : dislikes) {
g[p[0]].push_back(p[1]);
g[p[1]].push_back(p[0]);
}
for (int i = 1; i <= n; ++i) {
if (color[i] == 0 && !dfs(i, 1, color, g)) {
return false;
}
}
return true;
}
};
int main() {
int n;
cout << "Enter the number of people: ";
cin >> n;
vector<vector<int>> dislikes;
int m;
cout << "Enter the number of dislike pairs: ";
cin >> m;
cout << "Enter the dislike pairs (e.g., 1 2): " << endl;
for (int i = 0; i < m; ++i) {
int person1, person2;
cin >> person1 >> person2;
dislikes.push_back({ person1, person2 });
}
Solution solution;
bool isPossible = solution.possibleBipartition(n, dislikes);
if (isPossible) {
cout << "true." << endl;
}
else {
cout << "false." << endl;
}
return 0;
}
3.4.1 BFS系统运行、测试与分析
本地测试如下图3-1所示
图3-1 本地测试图
该算法使用了广度优先搜索来遍历图,通过颜色标记法判断不喜欢彼此的人是否可以分在不同的组。
运行与测试
- 将上述代码保存为一个.cpp文件,如BFS.cpp。
- 使用C++编译器编译这个文件,生成可执行文件。
- 运行可执行文件,按照提示输入行数和列数。
- 程序将输出计算得到的可能的二分法
分析
复杂度分析
时间复杂度:O(n+m),其中 n 题目给定的人数,m 为给定的 dislike 数组的大小。
空间复杂度:O(n+m),其中 n 题目给定的人数,m 为给定的 dislike数组的大小。
3.4.2 DFS算法系统运行、测试与分析
本地测试图如下图3-2所示
图3-2 本地测试图
该算法使用了深度优先搜索来遍历图,通过颜色标记法判断不喜欢彼此的人是否可以分在不同的组。
运行与测试
- 将上述代码保存为一个.cpp文件,如DFS.cpp。
- 使用C++编译器编译这个文件,生成可执行文件。
- 运行可执行文件,按照提示输入行数和列数。
- 程序将输出计算得到的最小路径之和。
分析
- 时间复杂度:O(n+m),其中 n 题目给定的人数,m 为给定的 dislike数组的大小。
- 空间复杂度:O(n+m),其中 n 题目给定的人数,m 为给定的 dislike 数组的大小。
在测试过程中,可以尝试不同的输入值,包括边界值和特殊情况,以验证算法的正确性和鲁棒性。