结对编程项目-最长英语单词链

本文介绍了结对编程项目,旨在实现最长英语单词链的计算,涉及信息隐藏、接口设计、松耦合等方法论。项目采用面向过程设计,提供CLI和GUI接口,详细描述了计算模块的实现、异常处理和测试方法。异常处理部分涵盖了命令行参数、文件和运行时可能出现的各种问题。此外,还讨论了GUI模块的设计和技术栈,以及前后端对接的松耦合实现。
摘要由CSDN通过智能技术生成

1 项目信息

2 预计耗时

PSP2.1 Personal Software Process Stages 预估耗时(分钟) 实际耗时(分钟)
Planning 计划
· Estimate · 估计这个任务需要多少时间 200
Development 开发
· Analysis · 需求分析 (包括学习新技术) 1200
· Design Spec · 生成设计文档 120
· Design Review · 设计复审 (和同事审核设计文档) 20
· Coding Standard · 代码规范 (为目前的开发制定合适的规范) 5
· Design · 具体设计 150
· Coding · 具体编码 1200
· Code Review · 代码复审 20
· Test · 测试(自我测试,修改代码,提交修改) 600
Reporting 报告
· Test Report · 测试报告 90
· Size Measurement · 计算工作量 60
· Postmortem & Process Improvement Plan · 事后总结, 并提出过程改进计划 20
合计 3685

3 方法论的运用

Information Hiding

我们在头文件中向外提供符号时,特地只包含了我们需要向外提供的函数。
由于我们采用了面向过程的设计风格,我们并没有运用 private 等访问控制方法做进一步的信息隐藏。

Interface Design

由于多样化的需求,我们一共提供了三套接口:

  1. 课程组约定的接口:
    EXPOSED_FUNCTION
    int gen_chain_word(const char* words[], int len, char* result[], char head, char tail, bool enable_loop);
    
    EXPOSED_FUNCTION
    int gen_chains_all(const char* words[], int len, char* result[]);
    
    EXPOSED_FUNCTION
    int gen_chain_word_unique(const char* words[], int len, char* result[]);
    
    EXPOSED_FUNCTION
    int gen_chain_char(const char* words[], int len, char* result[], char head, char tail, bool enable_loop);
    
  2. 面向 CLI 的接口:
    EXPOSED_FUNCTION
    int engine(
    	const char* words[],
    	int len,
    	char* result[],
    	char head,
    	char tail,
    	bool count,
    	bool weighted,
    	bool enable_self_loop,
    	bool enable_ring);
    
  3. 面向 GUI 的接口:
    EXPOSED_FUNCTION
    const char* gui_engine(
    	const char* input,
    	int type,
    	char head,
    	char tail,
    	bool weighted);
    

Loose Coupling

  • 课程组提供的接口这里我们提供了原样的实现,由于并不是我们设计的接口,在此不评价其耦合度。
  • 面向 CLI 的接口脱胎于课程组接口,其初衷是用一个函数管理四种模式,并方便命令行程序调用(直接传入各 flag)。由于其面向性较强,耦合度不可避免地相对较高。
  • 由于 GUI 模块与 Core 模块所属生态差异较大,因此必须做到尽量松的解耦。从接口的设计上体现这一点的是单词文本的输入方法和结果的返回方法。我们将输入、输出格式统一成了单一的字符串,方便在不同生态之间进行对接、交互。对于异常情况,此接口也要求在计算模块内部捕获异常,以文本形式返回信息,以尽可能地降低耦合度。

4 接口设计与实现

首先对所有单词去重,因为题目中要求同一个单词只能使用一次。

接着建立一个 26 26 26 个节点的有向图,每个节点代表一个字母。对于每个单词,设其首字母和尾字母分别为 h h h t t t,添加一条有向边 s → t s \to t st。该有向图的每个长度大于 2 2 2 的 Trail (不能经过同一条边的路径) 均对应一个单词链。

我们定义 exDAG 为这样的有向图:每个点至多有一条指向自己的边,且去除所有自环后,图是 DAG。

对于七种参数,大致可以将其分为如下四种类型:

  • -n,仅能单独使用,计算所有 Trail,有向图必须是 exDAG。
  • -m,仅能单独使用,不考虑所有自环计算最长 Trail,每条边边权为 1 1 1,有向图必须是 exDAG。
  • -w,计算最长 Trail,每条边边权为 1 1 1,可以搭配如下三种参数使用:
    • -h a,要求对应 Trail 的起点必须是字母 a
    • -t b,要求对应 Trail 的终点必须是字母 b
    • -r,若没有该参数则有向图必须是 exDAG。
  • -c,计算最长 Trail,每条边边权为单词长度,同样可以搭配上述三种参数使用。

我们实现了一个函数 int engine(const char* words[], int len, char* result[], char head, char tail, int type, bool weighted) 用于处理上述四种类型:

  • words 是传入每个单词的指针数组。
  • len 是传入单词的数量。
  • result 是存放所求结果的指针数组,同时该函数的返回值即为 result 对应数组的长度。
  • head 是对首字母的限制,若无限制为 0
  • tail 是对尾字母的限制,若无限制为 0
  • type 表示是上述哪种类型。
  • weighted 表示每条边的权值是否为单词长度。

该函数首先会调用 void init_words(const char* words[], int len) ,该函数将每个单词转化为对应的边。

接着会调用 void get_SCC(),该函数利用 Tarjan 算法求出有向图的每个强连通分量。

然后,如果要求有向图是 exDAG,会调用 void check_loop(),该函数先判断是否有一个点存在多于一个的自环,再判断是否存在一个强连通分量有多于两个点。若是,则抛出异常,该异常也会给出一个存在的单词环。

最后,根据要求类型的不同调用不同函数:

  • 若要求输出所有的单词链,则调用 int get_all(char* result[]),该函数通过枚举起点,DFS 找到所有单词链,若单词链数量超过 20000 20000 20000,则会抛出异常。

  • 若有向图是 exDAG,则调用 int get_max_DAG(char* result[], char head, char tail, bool enable_self_loop, bool weighted)。该函数的本质是在 DAG 上 dp 求最长路,这是一个很经典的问题,设 f i f_i fi 是以节点为 i i i 起点的最长路长度,对于一条边起点为 i i i、终点为 j j j、权值为 w w w 的边 e e e,通过逆序枚举拓扑序 (已经通过 Tarjan 算法求出),转移方程为:
    f i ← f j + w f_i \gets f_{j} + w fifj+w
    但该函数具体实现仍有许多改动的细节,列举如下:

    • 如果限定了终点 i i i,对于所有 j ≠ i j \ne i j=i f j f_j fj 初始化为 − ∞ -\infty f i f_i fi 初始化为 0 0 0;否则均初始化为 0 0 0
    • 计算最大值的同时需要记录方案。
    • 如果 enable_self_loop 为真,算完每个点 f i f_i fi 后要将其权值加到上面,同样需要改动方案的输出。
    • 因为最终要求路径长度大于 2 2 2,最终统计答案时需要枚举第一条边 ( i → j i \to j ij),且如果限制了起点需要保证其合法性,接着将这条边的权值 + f j +f_j +fj 作为答案,此外如果枚举的第一条边是自环还要进行特判。
  • 若有向图不要求为 exDAG,则调用 int get_max(char* result[], char head, char tail, bool weighted)。该函数的本质是状压 DP 求解一般有向图的最长 Trail。众所周知,一般有向图的最长 Trail 是一个 NP 问题,为保证正确性,我们没有采取近似算法。具体来说,设 f S , i f_{S,i} fS,i 是已使用的边集为 S S S,所在点为 i i i,以此为起始状态能经过的最长路长度,对于一条边起点为 i i i、终点为 j j j、权值为 w w w 的边 e e e,转移为:
    f S , i ← f S ∪ e , j + w f_{S,i} \gets f_{S \cup e,j}+w fS,ifSe,j+w
    具体实现上,因为最多只有 100 100 100 条边,可以使用 __int128 存储 S S S,但是我没有找到在 Visual Studio 中使用 __int128 的方法,因只需进行位运算,使用 pair<long long, long long> 代替。接着使用 map<pair<pair<long, long>, int>, int> 存储每个 f S , i f_{S,i} fS,i,进行记忆化搜索。

    该函数具体实现仍有许多改动的细节,均类似于函数 get_max_DAG,在此不进行赘述。

engine 函数并不对外暴露,为方便和 CLI 与 GUI 交互,还对其进行了封装。

为和另一组的 GUI 进行交互,我们封装了四个函数,它们的内部逻辑均为调用 engine 函数:

  • int gen_chain_word(const char* words[], int len, char* result[], char head, char tail, bool enable_loop)
  • int gen_chains_all(const char* words[], int len, char* result[])
  • int gen_chain_word_unique(const char* words[], int len, char* result[])
  • int gen_chain_char(const char* words[], int len, char* result[], char head, char tail, bool enable_loop)

为和我们的 CLI 进行交互,同时方便 CLI 读取选项,对 engine 函数重载为 int engine(const char* words[], int len, char* result[], char head, char tail, bool count, bool weighted, bool enable_self_loop, bool enable_ring),其内部逻辑也为调用原始 engine 函数。

为和我们的 GUI 进行交互,将 engine 函数封装为 const char* gui_engine(const char* input, int type, char head, char tail, bool weighted),其内部逻辑同样为调用原始 engine 函数。

这六个函数都被封装到 core.dll

5 计算模块 UML

由于我们的设计是面向过程的,这里展示的是我们计算模块的调用图。

6 性能改进

DAG 部分已经是线性的复杂度了,无法进一步优化复杂度。

非 DAG 部分,作为 NP 问题,在保证正确性的前提下,仅能做对一些特殊数据进行优化:

  • 如果当前点存在自环,优先走完全部自环。

  • 如果存在重边,只走权值最大的那条。

  • S S S 中仅保留和 i i i 属于同一个强连通分量的边,具体实现为当 i i i j j j 不在同一个强连通分量时,转移变为:
    f S , i ← f ∅ , j + w i , j f_{S,i} \gets f_{\varnothing,j}+w_{i,j} fS,if,j+wi,j

这些优化并不会改变该算法的时间复杂度,例如一个 10 10 10 个点的完全有向图,就可以令该算法达到指数级时间复杂度。

对于非构造样例,该算法的效率还是不错的,时间复杂度的瓶颈存在于每个强连通分量的内部,如果单个强连通分量内部的合法路径数量不是很多,可以很快计算出答案。

这里并没有额外花费时间进行更改,因为编码比较晚,所以编码之前已经全部想好。

5 5 5 个点的完全有向图为例,性能分析如下:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
时间主要花费在 dfs_max 、以及记忆化搜索过程中通过 map 查找上,按照我们的设计,这也是必然的。

7 合约编程

合约编程是形式化地描述接口以指导编程的一类编程方法。
其好处是可以精准地指定接口要求,无二义性地指导协作开发。
其缺点是目前技术限制下泛用性一般,且其在团队中将蕴含不菲的培训成本。

我们为节省开发和维护成本,主要采用口头约定的方法约定接口行为,其性价比较高。例如:

8 单元测试结果

计算模块部分单元测试主要有两部分组成:手工样例测试和对拍测试。

手工样例测试

主要是对题目中的样例、以及一些容易出错的小样例进行测试,下面以对 test_gen_chain_word 这个接口的测试为例。

首先通过函数 test 调用对应的接口,并和答案进行比对:

void test(const char* words[], int len, const char* ans[], int ans_len, char head, char tail, bool enable_loop){
   
    char** result = (char**)malloc
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ChatGPT结对编程是一种将两个程序员合作编写代码的技术。这种技术可以提高编写代码的效率和质量,同时也可以提高两位程序员的技能水平。以下是ChatGPT结对编程的步骤: 1. 首先,找到一个合适的编程伙伴。最好是一个有一定编程经验的人,但如果你是新手,也可以与另一个新手合作。 2. 确定你们编写的代码项目。你们可以选择一个共同感兴趣的项目或者一个有挑战性的项目。确保你们都对项目有一定的理解。 3. 确定你们的角色。一个人可以担任主要代码编写者,另一个人可以担任代码审核者。这样可以确保代码的质量。 4. 确定编程环境。你们可以使用一个共同的编程环境,如Visual Studio Code或者Atom。也可以使用在线编程环境,如CodePen或JSFiddle。 5. 开始编写代码。一个人负责编写代码,另一个人负责审核代码。在编写代码的过程中,你们可以随时通过聊天工具进行交流和讨论。 6. 定期进行代码审核。定期进行代码审核可以确保代码的质量。你们可以定期的分享代码,并相互审核对方的代码。 7. 完成项目并进行总结。完成项目后,你们可以总结你们的经验和教训,并提出改进建议。这将有助于你们以后更好的编写代码。 总之,ChatGPT结对编程是一种非常有用的技术,可以提高编写代码的效率和质量。通过合作编写代码,你们可以相互学习,相互支持,以及增强你们的编程技能。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值