301
社区成员
发帖
与我相关
我的任务
分享只多实现了一个counter类和Map类,前者用于异常计数,后者用于计算BlockSum和TripleSum。如下:

为什么这样做呢
使用独立的 Map 类来管理连通块数量和三元闭包数量,而不是直接在 MyNetwork 类中实现这些功能:
MyNetwork 类可能主要聚焦于管理人物间的网络关系,例如添加、删除人物和维护人际关系等。引入 Map 类可以使得 MyNetwork 不需要处理图的连通性和复杂的图算法,如连通分量和闭包计算,使代码更加模块化和易于管理。Map 类可以专门优化连通块和闭包的计算,包括数据结构的选择和算法的实现,而不必考虑网络的其他方面,如个体属性管理和人际关系的动态变化。person整体进行查询要快,同时使用了依赖注入,实现了解耦。Map 类可以使用专门的数据结构和算法(如并查集和邻接表),专注于图的效率和性能优化。例如,使用并查集可以非常快速地处理大量的连通性查询和合并操作。Map 可以独立地更新和计算,避免对整个网络数据的重复计算,特别是在大型数据集上操作时。Map 类中,使得未来对图算法的任何改进或优化都可以局部修改,而不影响整体网络逻辑。这对于添加新的功能或调整现有算法特别有用。Map 类可以单独进行单元测试,专注于图的连通性和闭包功能的正确性验证。这样可以更精确地控制测试覆盖范围,确保关键功能的稳定性和可靠性。Map 类可以减少 MyNetwork 与图的直接依赖,实现业务逻辑与数据处理逻辑的分离。这在处理复杂系统或多种类型的网络模型时尤其重要。综上所述,独立的 Map 类为系统的架构提供了更多的灵活性,同时提升了系统的可维护性和性能。这种方法允许开发者更有效地管理代码和优化特定功能,尤其是在大规模和高复杂性的应用场景中。
使用并查集来查询是否连通以及动态维护连通块blockCnt个数,使用一张图来动态维护三元闭包tripleCnt个数。几乎所有数据结构都用HashMap。
public int find(int x) {
...
parent.put(x, find(parent.get(x))); // 路径压缩
...
递归地调用 find 方法,直到找到根节点(当一个节点指向自身时,即为根节点)。在递归调用的过程中,每个节点的父节点都被更新为根节点,从而实现路径压缩。
按秩合并是一种优化并查集合并操作的方法。在这种策略中,每个元素都有一个“秩”(rank),通常可以理解为树的高度或深度。合并时,总是将秩较低的树的根指向秩较高的树的根。
public void union(int x, int y) {
...
if (rootX != rootY) {
if (rank.get(rootX) > rank.get(rootY)) {
parent.put(rootY, rootX);
}
...
else {
parent.put(rootY, rootX);
rank.put(rootX, rank.get(rootX) + 1); // 当两棵树的秩相等时,合并后的秩增加1
}
}
}
这段代码首先找到两个节点的根节点。如果它们不在同一个连通分量中,比较它们的秩,将秩较小的根节点的父节点更新为秩较大的根节点。如果秩相同,任选一个作为根,并增加其秩。
BlockCntadd节点的时候++,union的时候--即可
唯一需要注意的点就是如何在并查集中删除一个关系呢,朴素的全部重建我觉得也不会超时,并且在互测数据范围内不会被卡超时。当然可以选择只重建那两个节点的相关部分,方法如下:
两个相连的点A,B,对A进行dfs,把所有跟A有关系的点的根设置为A,同时秩为1。
for (int node : visited) {
parent.put(node, startNode);
rank.put(node, 1);
}
如果这其中没有B,就把B新建为一个根节点。
(zy学姐的博客中貌似说使用这种方法不能使用按秩合并,为什么,不懂)
dfs
private void dfs(int node, Set<Integer> visited) {
visited.add(node);
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
dfs(neighbor, visited);
}
}
}
tripleCnt因为在Map类中额外维护了一个关系图,在union和removeEdge的时候分别去addTriple(x, y);,delTriple(x, y);
朴素的遍历,唯一的小优化是先比较x和y谁的有关系节点少,就用他的查
private void addTriple(int x, int y) {
for (int neighbor : graph.get(x)) {
if (graph.get(neighbor).contains(y)) {
tripleCnt++;
}
}
}
在上面这个操作中
private void addTriple(int x, int y) {
for (int neighbor : graph.get(x)) {
if (graph.get(neighbor).contains(y)) {
tripleCnt++;
}
}
}
这样写
if (graph.get(y).contains(neighbor))
在我的架构中是可以的,因为使用HashMap。但如果是在MyPerson中进行三元闭包的添加,就要注意这个顺序了,就是数组按行遍历和按列遍历,实测速度会相差10倍。
或许可以使用动态树Link/Cut Trees?支持边的动态添加和删除。Link/Cut Trees 结构使用 Splay Trees 作为其核心的数据结构来处理动态树的问题。由于 Splay Trees 的操作通常涉及到旋转,以使任意节点成为树的根,Link/Cut Trees 的性能取决于 Splay Tree 的操作效率。时间复杂度均为 **O(log n)**。
相较于并查集,Link/Cut Trees 提供了更多的灵活性,尤其是在需要处理节点间动态连接和断开的场景下。并查集在处理只有合并操作的连通性问题时非常高效,尤其是通过路径压缩和按秩合并优化后,其几乎能达到接近常数时间的效率。
使用150个节点构建成一个完全图(11175个边),然后再删除所有的边,嗯不超时。

junit注意以下pure条件就好
这次作业要实现的内容有以下几个:
增加tag,并求总和和方差
找value最大的熟人,并且计算couplesum
求两点间的最短路径(不带权重)
首先$10000 * 200 ^ 2 = 4*10^8 < int:2,147,483,6472,147,483,647$
也就是说方差不会爆int
然后就是在tag中维护两个属性
private int ageSum;
private int ageSquaredSum;
在addPerson,delPerson的时候维护一下就好
couplesum首先是如何去求一个的bestperson,以下用bp代称
这里朴素实现的话就是遍历,但是我们可以用一个带有顺序的数据结构去存acquaintance,我使用的是treemap,前面的键值就是value,同时按照value从大到小排序
private final TreeMap<Integer, Set<Person>> acquaintancesByValue = new TreeMap<>(Collections.reverseOrder());
这样的话理论上来讲,add操作由常数级变为Olgn,查找bp操作由On变为了常数级(取第一个值就好)
但是实际测试的话与正常实现的时间相差不大。
注意一下加人的时候还要按照id从小到大加进去
acquaintancesByValue.putIfAbsent(value, new TreeSet<>(Comparator.comparingInt(Person::getId)));
acquaintancesByValue.get(value).add(person);
这里同样使用treeSet来实现排序
在您的当前实现中,确实,使用 TreeMap<Integer, Set<Integer>> 嵌套 TreeSet<Integer> 的结构,每次添加和删除操作的时间复杂度都是log2n,其中 (n) 是元素的数量。这种结构确保了亲密值可以被排序,并且每个亲密值下的熟人 ID 也是有序的。
改进方案
如果您想避免使用两层的排序结构而又想维持排序的特性,可以考虑使用以下几种方法之一:
使用单个数据结构,如 TreeMap,但以不同的方式:
TreeMap 的 key 设计为一个复合键,这个键可以同时包含亲密值和熟人 ID,但这需要实现自定义的比较器。这种方式的缺点是处理复杂性高,且可能难以直接使用标准的 TreeMap。使用 PriorityQueue 或 SortedSet 的变种:
PriorityQueue 或 SortedSet(如 TreeSet),这些结构内部也是用树实现的,但可以通过封装一个包含亲密值和 ID 的对象来管理排序。自定义数据结构:
使用自定义比较器
如何使用 TreeMap 但通过自定义比较器来避免嵌套操作:
this.acquaintances = new TreeMap<>(Comparator
.comparingInt(AcquaintanceKey::getValue)
.thenComparingInt(AcquaintanceKey::getPersonId));
private static class AcquaintanceKey {
private final int value;
private final int personId;
}
1
public class MyPerson implements Person {
private final int id;
private final String name;
private final int age;
private final TreeMap<AcquaintanceKey, Integer> acquaintances = new TreeMap<>();
private final HashMap<Integer, Tag> tags;
public MyPerson(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
this.tags = new HashMap<>();
}
private static class AcquaintanceKey {
int value;
int personId;
AcquaintanceKey(int value, int personId) {
this.value = value;
this.personId = personId;
}
public int getValue() {
return value;
}
public int getPersonId() {
return personId;
}
}
private Comparator<AcquaintanceKey> comparator = Comparator
.comparingInt(AcquaintanceKey::getValue)
.thenComparingInt(AcquaintanceKey::getPersonId);
public void addAcquaintance(Person person, int value) {
AcquaintanceKey key = new AcquaintanceKey(value, person.getId());
acquaintances.put(key, person.getId());
}
// 其他方法同理进行修改
}
这种方法通过将亲密值和熟人 ID 封装成一个 AcquaintanceKey 对象,然后在 TreeMap 中使用自定义的比较器来排序。这样可以避免嵌套的集合结构,同时保持数据的有序性和快速访问性。
之后就是求couplesum了
我在这里实现了一个脏位,因为能够使得couplesum改变的情况是可以被包含在下面两种情况中的:
在modifyRelation中增加对这两种情况的检查,成立就设置脏位为true
在queryCoupleSum中
if (!coupleDirty) {
return coupleCnt;
}
coupleCnt = 0;
coupleDirty = false;
这样就实现了尽可能少的全部遍历着查找couplesum
同时,还可以用一个set来存已经从处理过的person,当遍历到这个person的时候,continue就好,如下,这样这个操作就是On级别的了
for (Person p1 : persons.values()) {
获取p1的最佳熟人ID
如果p1已处理过,则跳过
获取p1的最佳熟人
获取p2的最佳熟人ID
如果p2的最佳熟人也是p1
计数
标记p1,p2为已处理
}
}
}
上次作业中我在Mymap类中预存了一个关系图,这时候就用上了,可以基于此来实现对最小路径的查询。
queryShortestPath(int id1, int id2) 是在不带权重的图中寻找路径。使用BFS比较合理
广度优先搜索(BFS)
BFS 是一种用于图的遍历或搜索的算法,它从根节点开始,一层层向外扩展,直到找到目标节点为止,因此它自然地适用于在无权图中找到最短路径(即边的数量最少的路径)。
在此基础上,我实现了双向BFS,如下:
双向广度优先搜索(Bi-directional BFS)是一种用于搜索最短路径的算法,特别适用于在两个已知节点之间寻找最短路径的场景。这种方法从两个节点(通常是起点和终点)同时开始搜索,一层一层地向外扩展,直到两个搜索从不同方向相遇。相比于单向的 BFS,双向 BFS 可以显著减少搜索空间,因此在很多情况下能更快地找到最短路径。
双向 BFS 同时从两个节点开始搜索:一个从起点开始,另一个从终点开始。每次迭代中,算法交替扩展这两个方向的边界,即先扩展从起点开始的一层,再扩展从终点开始的一层。这种方法的关键在于,只需要扩展到两个搜索相遇的地方,而不是完全探索整个图。
dijkstra比较我们可不可以把查找过程中的点之间的距离都存下来呢,下次找的时候就可以直接给出了。这里同样需要设置脏位。但是对于单向BFS来说这个操作很好实现,那么双向BFS怎么办呢。
双向 BFS 和单向 BFS 加缓存是两种有效的最短路径搜索策略,它们各有优势和局限。下面是对这两种方法的分析:
选择哪种方法取决于具体应用的需求:
在考虑100个节点的稠密图和稀疏图进行最短路径搜索时,我们可以分析双向 BFS 和单向 BFS(加缓存)的效率。对于这样的图规模,我们可以详细比较这两种方法在不同类型的图结构下的表现。
对于稠密图:
对于稀疏图:
综上,可以找一个折中的方法:就是在双向BFS的时候左边存一半,右边存一半经过点的距离。
private int visitLevel() {
得到当前层的节点数
for (int i = 0; i < currentSize; i++) {
得到当前距离
for (int neighbor : graph.get(currentNode)) {
if (!visited.containsKey(neighbor)) {
存储距离时考虑中间节点数量
检查是否与对方搜索相遇
updateCache(sourceId, neighbor, totalDistance);
检查是否与对方搜索相遇 {
// 返回总距离
updateCache(sourceId, neighbor, totalDistance);
return totalDistance;
}
}
}
}
}
}
return -1; // 这一层未找到交汇点
}
我的nb帅气室友叶佩霖提出了:
当删边前后的tripleSum改变了,就说明并查集去掉这俩点的连接后,有另外一个点跟他们是连着的!!!!!因此并查集不需要重建,这个剪枝操作实测能让删除操作的耗时降低一个数量级。
private final List<Message> messages = new LinkedList<>();// 使用 LinkedList 存储消息,因为要取前五个
1
惨淡出现两个bug
第一个就是queryValueSum爆掉了,设置了脏位
第二个就是
person1.addMoney(-moneyToDistribute * tag.getSize());
这里直接写的
person1.addMoney(((RedEnvelopeMessage) message).getMoney());
money可能不会整除,寄
只多实现了一个counter类和Map类,前者用于异常计数,后者用于计算BlockSum和TripleSum。如下:

为什么这样做呢
使用独立的 Map 类来管理连通块数量和三元闭包数量,而不是直接在 MyNetwork 类中实现这些功能:
MyNetwork 类可能主要聚焦于管理人物间的网络关系,例如添加、删除人物和维护人际关系等。引入 Map 类可以使得 MyNetwork 不需要处理图的连通性和复杂的图算法,如连通分量和闭包计算,使代码更加模块化和易于管理。Map 类可以专门优化连通块和闭包的计算,包括数据结构的选择和算法的实现,而不必考虑网络的其他方面,如个体属性管理和人际关系的动态变化。person整体进行查询要快,同时使用了依赖注入,实现了解耦。Map 类可以使用专门的数据结构和算法(如并查集和邻接表),专注于图的效率和性能优化。例如,使用并查集可以非常快速地处理大量的连通性查询和合并操作。Map 可以独立地更新和计算,避免对整个网络数据的重复计算,特别是在大型数据集上操作时。Map 类中,使得未来对图算法的任何改进或优化都可以局部修改,而不影响整体网络逻辑。这对于添加新的功能或调整现有算法特别有用。Map 类可以单独进行单元测试,专注于图的连通性和闭包功能的正确性验证。这样可以更精确地控制测试覆盖范围,确保关键功能的稳定性和可靠性。Map 类可以减少 MyNetwork 与图的直接依赖,实现业务逻辑与数据处理逻辑的分离。这在处理复杂系统或多种类型的网络模型时尤其重要。综上所述,独立的 Map 类为系统的架构提供了更多的灵活性,同时提升了系统的可维护性和性能。这种方法允许开发者更有效地管理代码和优化特定功能,尤其是在大规模和高复杂性的应用场景中。
使用并查集来查询是否连通以及动态维护连通块blockCnt个数,使用一张图来动态维护三元闭包tripleCnt个数。几乎所有数据结构都用HashMap。
public int find(int x) {
...
parent.put(x, find(parent.get(x))); // 路径压缩
...
递归地调用 find 方法,直到找到根节点(当一个节点指向自身时,即为根节点)。在递归调用的过程中,每个节点的父节点都被更新为根节点,从而实现路径压缩。
按秩合并是一种优化并查集合并操作的方法。在这种策略中,每个元素都有一个“秩”(rank),通常可以理解为树的高度或深度。合并时,总是将秩较低的树的根指向秩较高的树的根。
public void union(int x, int y) {
...
if (rootX != rootY) {
if (rank.get(rootX) > rank.get(rootY)) {
parent.put(rootY, rootX);
}
...
else {
parent.put(rootY, rootX);
rank.put(rootX, rank.get(rootX) + 1); // 当两棵树的秩相等时,合并后的秩增加1
}
}
}
这段代码首先找到两个节点的根节点。如果它们不在同一个连通分量中,比较它们的秩,将秩较小的根节点的父节点更新为秩较大的根节点。如果秩相同,任选一个作为根,并增加其秩。
BlockCntadd节点的时候++,union的时候--即可
唯一需要注意的点就是如何在并查集中删除一个关系呢,朴素的全部重建我觉得也不会超时,并且在互测数据范围内不会被卡超时。当然可以选择只重建那两个节点的相关部分,方法如下:
两个相连的点A,B,对A进行dfs,把所有跟A有关系的点的根设置为A,同时秩为1。
for (int node : visited) {
parent.put(node, startNode);
rank.put(node, 1);
}
如果这其中没有B,就把B新建为一个根节点。
(zy学姐的博客中貌似说使用这种方法不能使用按秩合并,为什么,不懂)
dfs
private void dfs(int node, Set<Integer> visited) {
visited.add(node);
for (int neighbor : graph.get(node)) {
if (!visited.contains(neighbor)) {
dfs(neighbor, visited);
}
}
}
tripleCnt因为在Map类中额外维护了一个关系图,在union和removeEdge的时候分别去addTriple(x, y);,delTriple(x, y);
朴素的遍历,唯一的小优化是先比较x和y谁的有关系节点少,就用他的查
private void addTriple(int x, int y) {
for (int neighbor : graph.get(x)) {
if (graph.get(neighbor).contains(y)) {
tripleCnt++;
}
}
}
在上面这个操作中
private void addTriple(int x, int y) {
for (int neighbor : graph.get(x)) {
if (graph.get(neighbor).contains(y)) {
tripleCnt++;
}
}
}
这样写
if (graph.get(y).contains(neighbor))
在我的架构中是可以的,因为使用HashMap。但如果是在MyPerson中进行三元闭包的添加,就要注意这个顺序了,就是数组按行遍历和按列遍历,实测速度会相差10倍。
或许可以使用动态树Link/Cut Trees?支持边的动态添加和删除。Link/Cut Trees 结构使用 Splay Trees 作为其核心的数据结构来处理动态树的问题。由于 Splay Trees 的操作通常涉及到旋转,以使任意节点成为树的根,Link/Cut Trees 的性能取决于 Splay Tree 的操作效率。时间复杂度均为 **O(log n)**。
相较于并查集,Link/Cut Trees 提供了更多的灵活性,尤其是在需要处理节点间动态连接和断开的场景下。并查集在处理只有合并操作的连通性问题时非常高效,尤其是通过路径压缩和按秩合并优化后,其几乎能达到接近常数时间的效率。
使用150个节点构建成一个完全图(11175个边),然后再删除所有的边,嗯不超时。

junit注意以下pure条件就好
这次作业要实现的内容有以下几个:
增加tag,并求总和和方差
找value最大的熟人,并且计算couplesum
求两点间的最短路径(不带权重)
首先$10000 * 200 ^ 2 = 4*10^8 < int:2,147,483,6472,147,483,647$
也就是说方差不会爆int
然后就是在tag中维护两个属性
private int ageSum;
private int ageSquaredSum;
在addPerson,delPerson的时候维护一下就好
couplesum首先是如何去求一个的bestperson,以下用bp代称
这里朴素实现的话就是遍历,但是我们可以用一个带有顺序的数据结构去存acquaintance,我使用的是treemap,前面的键值就是value,同时按照value从大到小排序
private final TreeMap<Integer, Set<Person>> acquaintancesByValue = new TreeMap<>(Collections.reverseOrder());
这样的话理论上来讲,add操作由常数级变为Olgn,查找bp操作由On变为了常数级(取第一个值就好)
但是实际测试的话与正常实现的时间相差不大。
注意一下加人的时候还要按照id从小到大加进去
acquaintancesByValue.putIfAbsent(value, new TreeSet<>(Comparator.comparingInt(Person::getId)));
acquaintancesByValue.get(value).add(person);
这里同样使用treeSet来实现排序
在您的当前实现中,确实,使用 TreeMap<Integer, Set<Integer>> 嵌套 TreeSet<Integer> 的结构,每次添加和删除操作的时间复杂度都是log2n,其中 (n) 是元素的数量。这种结构确保了亲密值可以被排序,并且每个亲密值下的熟人 ID 也是有序的。
改进方案
如果您想避免使用两层的排序结构而又想维持排序的特性,可以考虑使用以下几种方法之一:
使用单个数据结构,如 TreeMap,但以不同的方式:
TreeMap 的 key 设计为一个复合键,这个键可以同时包含亲密值和熟人 ID,但这需要实现自定义的比较器。这种方式的缺点是处理复杂性高,且可能难以直接使用标准的 TreeMap。使用 PriorityQueue 或 SortedSet 的变种:
PriorityQueue 或 SortedSet(如 TreeSet),这些结构内部也是用树实现的,但可以通过封装一个包含亲密值和 ID 的对象来管理排序。自定义数据结构:
使用自定义比较器
如何使用 TreeMap 但通过自定义比较器来避免嵌套操作:
this.acquaintances = new TreeMap<>(Comparator
.comparingInt(AcquaintanceKey::getValue)
.thenComparingInt(AcquaintanceKey::getPersonId));
private static class AcquaintanceKey {
private final int value;
private final int personId;
}
1
public class MyPerson implements Person {
private final int id;
private final String name;
private final int age;
private final TreeMap<AcquaintanceKey, Integer> acquaintances = new TreeMap<>();
private final HashMap<Integer, Tag> tags;
public MyPerson(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
this.tags = new HashMap<>();
}
private static class AcquaintanceKey {
int value;
int personId;
AcquaintanceKey(int value, int personId) {
this.value = value;
this.personId = personId;
}
public int getValue() {
return value;
}
public int getPersonId() {
return personId;
}
}
private Comparator<AcquaintanceKey> comparator = Comparator
.comparingInt(AcquaintanceKey::getValue)
.thenComparingInt(AcquaintanceKey::getPersonId);
public void addAcquaintance(Person person, int value) {
AcquaintanceKey key = new AcquaintanceKey(value, person.getId());
acquaintances.put(key, person.getId());
}
// 其他方法同理进行修改
}
这种方法通过将亲密值和熟人 ID 封装成一个 AcquaintanceKey 对象,然后在 TreeMap 中使用自定义的比较器来排序。这样可以避免嵌套的集合结构,同时保持数据的有序性和快速访问性。
之后就是求couplesum了
我在这里实现了一个脏位,因为能够使得couplesum改变的情况是可以被包含在下面两种情况中的:
在modifyRelation中增加对这两种情况的检查,成立就设置脏位为true
在queryCoupleSum中
if (!coupleDirty) {
return coupleCnt;
}
coupleCnt = 0;
coupleDirty = false;
这样就实现了尽可能少的全部遍历着查找couplesum
同时,还可以用一个set来存已经从处理过的person,当遍历到这个person的时候,continue就好,如下,这样这个操作就是On级别的了
for (Person p1 : persons.values()) {
获取p1的最佳熟人ID
如果p1已处理过,则跳过
获取p1的最佳熟人
获取p2的最佳熟人ID
如果p2的最佳熟人也是p1
计数
标记p1,p2为已处理
}
}
}
上次作业中我在Mymap类中预存了一个关系图,这时候就用上了,可以基于此来实现对最小路径的查询。
queryShortestPath(int id1, int id2) 是在不带权重的图中寻找路径。使用BFS比较合理
广度优先搜索(BFS)
BFS 是一种用于图的遍历或搜索的算法,它从根节点开始,一层层向外扩展,直到找到目标节点为止,因此它自然地适用于在无权图中找到最短路径(即边的数量最少的路径)。
在此基础上,我实现了双向BFS,如下:
双向广度优先搜索(Bi-directional BFS)是一种用于搜索最短路径的算法,特别适用于在两个已知节点之间寻找最短路径的场景。这种方法从两个节点(通常是起点和终点)同时开始搜索,一层一层地向外扩展,直到两个搜索从不同方向相遇。相比于单向的 BFS,双向 BFS 可以显著减少搜索空间,因此在很多情况下能更快地找到最短路径。
双向 BFS 同时从两个节点开始搜索:一个从起点开始,另一个从终点开始。每次迭代中,算法交替扩展这两个方向的边界,即先扩展从起点开始的一层,再扩展从终点开始的一层。这种方法的关键在于,只需要扩展到两个搜索相遇的地方,而不是完全探索整个图。
dijkstra比较我们可不可以把查找过程中的点之间的距离都存下来呢,下次找的时候就可以直接给出了。这里同样需要设置脏位。但是对于单向BFS来说这个操作很好实现,那么双向BFS怎么办呢。
双向 BFS 和单向 BFS 加缓存是两种有效的最短路径搜索策略,它们各有优势和局限。下面是对这两种方法的分析:
选择哪种方法取决于具体应用的需求:
在考虑100个节点的稠密图和稀疏图进行最短路径搜索时,我们可以分析双向 BFS 和单向 BFS(加缓存)的效率。对于这样的图规模,我们可以详细比较这两种方法在不同类型的图结构下的表现。
对于稠密图:
对于稀疏图:
综上,可以找一个折中的方法:就是在双向BFS的时候左边存一半,右边存一半经过点的距离。
private int visitLevel() {
得到当前层的节点数
for (int i = 0; i < currentSize; i++) {
得到当前距离
for (int neighbor : graph.get(currentNode)) {
if (!visited.containsKey(neighbor)) {
存储距离时考虑中间节点数量
检查是否与对方搜索相遇
updateCache(sourceId, neighbor, totalDistance);
检查是否与对方搜索相遇 {
// 返回总距离
updateCache(sourceId, neighbor, totalDistance);
return totalDistance;
}
}
}
}
}
}
return -1; // 这一层未找到交汇点
}
我的nb帅气室友叶佩霖提出了:
当删边前后的tripleSum改变了,就说明并查集去掉这俩点的连接后,有另外一个点跟他们是连着的!!!!!因此并查集不需要重建,这个剪枝操作实测能让删除操作的耗时降低一个数量级。
private final List<Message> messages = new LinkedList<>();// 使用 LinkedList 存储消息,因为要取前五个
本单元的测试过程包括以下几个步骤:
数据生成:
指令执行:
ap)、添加关系(ar)、查询(qv、qci等)、发送消息(am、arem、anm等)等。结果验证:
黑箱测试:
白箱测试:
单元测试:
功能测试:
集成测试:
压力测试:
回归测试:
根据不同的需求和规则生成相应的指令字符串。
随机性:
Random类生成随机数和字符串,确保测试数据的多样性和不可预测性。列表管理:
指令多样性:
数据关联:
tags)和已生成的消息ID、表情ID,确保生成的数据具有一定的关联性和逻辑性。组合测试
组合测试是将多种指令组合起来进行测试,验证系统在不同操作组合下的表现
大数据量测试
大数据量测试是测试系统在处理大量数据时的表现。这有助于发现系统的性能瓶颈和资源管理问题。
. 特殊值测试
特殊值测试包括零值、负值和特定的特殊值。此策略有助于发现处理特殊值时的潜在问题。
测试过程涵盖了从数据生成到结果验证的完整流程,采用了多种数据生成策略来覆盖不同的测试场景和条件。通过黑箱测试和白箱测试结合的方式,可以确保系统在各种情况下的稳定性和正确性。单元测试、功能测试、集成测试、压力测试和回归测试等不同类型的测试共同作用,全面保证系统的质量。
本单元的架构设计包括以下几个核心组件:
MyPerson对象,并处理节点和节点之间关系的增删改查。MyNetwork的操作。MyMessage、MyNoticeMessage、MyEmojiMessage、MyRedEnvelopeMessage,处理不同类型的消息。MyTag表示标签,用于组织和管理用户的标签信息。这些组件共同构成了一个社交网络图模型,支持用户、关系、消息和标签的管理。
图模型的构建策略包括以下几个方面:
节点的创建:
MyPerson对象,包含用户的基本信息(如ID、名字、年龄)和朋友关系。MyNetwork的addPerson方法来创建和添加新的节点。边的创建:
MyNetwork的addRelation方法来创建。消息的处理:
MyNetwork或MyMap的方法来发送和处理消息,确保消息在用户之间正确传递。标签的管理:
MyTag类表示标签信息,通过标签将用户组织起来。MyNetwork或MyMap的方法来添加和管理标签,支持用户对标签的操作。图模型的维护策略主要包括以下几个方面:
节点的维护:
MyNetwork的addPerson方法添加新节点。MyNetwork的removePerson方法,同时需要从所有节点的朋友列表中移除该节点,保持图的完整性。MyPerson对象的方法来更新节点信息,如修改年龄或名字。边的维护:
MyNetwork的addRelation方法,确保双方的朋友列表都被更新。MyNetwork的removeRelation方法,同样需要更新双方的朋友列表。消息的维护:
MyMessage、MyNoticeMessage)来管理不同类型的消息,确保消息的正确传递和处理。标签的维护:
MyTag类来管理标签信息,支持标签的创建、删除和更新。MyNetwork或MyMap的方法来处理标签相关的操作,确保标签信息的准确性。具体实现细节见上面的作业总结
出现了两个bug
1.queryValueSum被卡到了,设置脏位解决
2.
person1.addMoney(-moneyToDistribute * tag.getSize());
写成了
person1.addMoney(-(RedEnvelopeMessage) message).getMoney());
可能不被整除,没好好读JML导致的。
规格与实现分离是一种软件工程中的重要原则,它强调在开发过程中将软件的行为描述(规格)与其具体实现分开。这样做有几个主要优点:
JML(Java Modeling Language)作为一种行为接口规格语言,与规格与实现分离的原则密切相关。
JML允许开发者在Java代码中嵌入行为规范,包括前置条件、后置条件、类不变式等。这些规范明确地描述了方法的预期行为和类的属性,而不涉及具体的实现细节。例如,前置条件定义了在调用方法之前必须满足的条件,后置条件定义了方法执行后应满足的条件。这些规范使得开发者和用户可以清晰地理解系统的预期行为,而无需了解其内部实现。
通过使用JML,开发者可以将规格与实现分开,这大大提高了代码的可维护性。当需求发生变化时,开发者可以首先修改JML规范,然后再调整实现代码。JML规范提供了一个明确的行为标准,确保实现的修改是有依据的,并且不会引入意外的错误。
如果规格与实现严格分离,可以在不影响外部行为的情况下替换实现。JML提供了一个详细的行为描述,使得不同的实现可以基于相同的规格进行替换。这样,如果需要优化性能或进行技术更新,只需确保新实现满足JML规范即可,而不需要担心改变外部行为。
JML规范可以用于生成测试用例,根据规格进行测试可以验证系统是否满足预期需求。由于JML明确了方法的行为规范,测试人员可以根据这些规范设计测试用例,独立于实现进行测试。这确保了测试的全面性和准确性,能够有效发现实现中的问题。
JML规范作为代码的一部分,提供了自文档化的效果。通过阅读JML注释,开发者可以直接理解类和方法的行为规范,这对于代码审查、团队协作和后续维护非常有帮助。
JML与规格与实现分离的关系密切,通过提供形式化的行为规范,JML实现了规格与实现的明确分离。这种分离带来了诸多好处,包括明确行为、增强可维护性、支持可替换性、改进测试性和提升文档化效果,使得软件系统更加健壮、易于理解和维护。
结合JML规格信息,我们可以更好地设计JUnit测试,并有效检验代码实现与规格的一致性。以下是如何利用规格信息优化JUnit测试设计的分析:
规格信息提供了详细的行为描述,包括前置条件、后置条件和类不变式。利用这些信息,可以明确测试的目标和预期结果。
利用JML规格信息,可以设计出全面的测试用例,覆盖各种可能的输入和场景。
一些工具可以根据JML规格自动生成测试用例,这大大简化了测试的设计过程,并确保测试用例与规格的一致性。
通过结合JML规格信息,JUnit测试用例可以更清晰地表达测试目的和预期结果,提高测试代码的可读性和可维护性。
在JUnit测试中,可以首先验证前置条件是否满足,确保在适当的情况下调用方法。
在方法调用后,使用JUnit断言验证后置条件是否满足。
通过在方法调用前后验证类不变式,确保类在操作过程中始终保持一致性。
通过利用规格信息,可以设计出全面的测试用例,覆盖更多的场景和边界情况,从而提高测试覆盖率。
JUnit测试可以验证实现是否满足JML规格,通过自动化测试不断验证代码的正确性和一致性,确保实现与规格一致。
规格信息提供了明确的测试目标和预期结果,简化了测试用例的设计过程,使测试更具针对性和有效性。
通过全面的JUnit测试,验证代码实现是否满足规格,可以有效发现和修复潜在的错误,增强代码的可靠性和稳定性。
结合JML规格信息,利用JUnit测试可以更好地设计和实现测试用例,全面验证代码实现与规格的一致性。这种方法不仅提高了测试覆盖率和代码可靠性,还简化了测试设计过程,使得测试更加高效和有针对性。通过这种方式,可以有效确保软件系统的行为符合预期,提升软件质量。
在本单元学习过程中,JML(Java Modeling Language)给我留下了深刻的印象。作为一种行为接口规格语言,JML在描述Java程序行为方面提供了强大的工具,使得规格与实现分离的原则得以有效应用。以下是我对JML的几点体会:
JML的主要作用是通过形式化的方式描述程序的行为,包括方法的前置条件、后置条件和类不变式等。这种明确的行为描述大大增强了代码的清晰性,使得开发者能够准确理解和维护代码。
JML使得代码维护更加容易。当需求变化或代码需要重构时,可以首先修改JML规范,然后根据新的规范调整代码实现。这样做有助于确保修改后的代码仍然符合预期行为,减少引入错误的风险。此外,JML规范还为代码的文档化提供了极大的帮助,使得代码更加可读和易于理解。
JML不仅仅是文档化工具,它还可以与工具链结合,进行自动化验证和测试。通过JML,可以自动生成测试用例,并在运行时验证程序是否满足规格要求。例如:
这种自动化验证和测试极大地提高了开发效率和代码质量。
JML促使开发者在编写代码前首先思考并定义行为规范,这有助于设计出更加健壮和可靠的软件系统。通过详细定义每个类和方法的行为,可以提前发现和解决潜在的问题,减少在实现阶段出现的错误。
本单元强调了规格与实现分离的重要性,并通过JML提供了实现这一原则的具体工具。通过在代码中嵌入JML规范,我们可以清晰地定义和理解系统的行为,而不依赖于具体的实现细节。这种方法提高了代码的可读性和可维护性,使得代码修改和扩展更加容易。
在实际开发中,JUnit测试是确保代码质量的常用工具。本单元通过结合JML和JUnit测试,展示了如何利用规格信息设计和实现更为全面和有效的测试。通过JML规范明确测试目标,JUnit测试用例可以覆盖更多的场景和边界情况,从而提高测试覆盖率和代码可靠性。
在实践过程中,编写和维护JML规范需要一定的时间和精力,尤其是在复杂系统中。然而,这种投入是值得的,因为它能显著提高代码质量,减少后续维护的成本和风险。此外,通过结合JML和JUnit测试,可以形成良好的开发和测试习惯,提升整体开发效率和软件质量。
通过本单元的学习,我深刻体会到JML在增强代码清晰性、提高可维护性、支持自动化验证和测试以及促进可靠的软件设计方面的巨大价值。同时,结合JML和JUnit测试,实现了规格与实现分离的原则,使得代码开发更加规范和高效。