DFS+String 单词接龙

单词接龙

题目描述

单词接龙是一个与我们经常玩的成语接龙相类似的游戏,现在我们已知一组单词,且给定一个开头的字母,要求出以这个字母开头的最长的“龙”(每个单词都最多在“龙”中出现两次),在两个单词相连时,其重合部分合为一部分,例如 beast 和 astonish,如果接成一条龙则变为 beastonish,另外相邻的两部分不能存在包含关系,例如 at 和 atide 间不能相连。


输入

输入的第一行为一个单独的整数 n 表示单词数,以下 n 行每行有一个单词,输入的最后一行为一个单个字符,表示“龙”开头的字母。你可以假定以此字母开头的“龙”一定存在。


输出

只需输出以此字母开头的最长的“龙”的长度,输出占一行。


样例输入

5
At
Touch
Cheat
Choose
Tact
A


样例输出

23


提示

条件:n≤20
样例解释:连成的“龙”为 atoucheatactactouchoose。


题目来源

P1019 [NOIP2000 提高组] 单词接龙


完整代码如下

#include<cstdio>
#include<iostream>
using namespace std;
int n, ans, vis[22];
string a[22];
//第一个参数是上一个字符串
//第二个参数是拼接后的长度 
void dfs(string x, int s)
{
    //求最长长度
    ans = max(ans, s);
    //枚举每一个字符串 
    for (int i = 1; i <= n; i++) {
        int p = 1;//重叠部分长度 
        int la = x.length();
        int lb = a[i].length();
        while (p < min(la, lb)) {
            if (x.substr(la - p) == a[i].substr(0, p) && vis[i] < 2) {
                vis[i]++;
                dfs(a[i], s + lb - p);
                vis[i]--;
                break;
            }
            p++;
        }
    }
    return;
}
int main()
{
    //读入 
    cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    char t;
    cin >> t;
    //枚举找到首字母为t的字符串 
    for (int i = 1; i <= n; i++) {
        if (a[i][0] == t) {
            vis[i]++;//vis数组解决搜两次的问题 
            dfs(a[i], a[i].length());
            vis[i]--;
        }
    }
    cout << ans;
    return 0;
}

思路详解

(1)这是什么类型的题?

1)因为要判断String能否连接所以必然为一道字符串

2)我们要将字符串连接起来,其中最小的子问题是两个字符串的连接问题,即最开始的情况下若有n个字符串,我们连接其中两个字符串,在确认第一个单词之后,剩下的一个单词的选择有n或n-1种情况(每个单词只能用两次)然后继续往下连接时还可能有n或n-1种情况,每一层连接都需要for循环所有的字符串,而且是不断往下进行的,所以也是一道深度搜索问题DFS问题


(2)代码实现

第一步:读入

cin >> n;
    for (int i = 1; i <= n; i++) {
        cin >> a[i];
    }
    char t;
    cin >> t;

第二步:找到第一个单词开始DFS

 for (int i = 1; i <= n; i++) {
        if (a[i][0] == t) {
            vis[i]++;//vis数组解决搜两次的问题
            //第一个字母使用过一次 
            dfs(a[i], a[i].length());
            vis[i]--;
        }
    }

问题一:为何只传入字符串和其长度可以?
答:因为如果我们知道两个字符串可以拼接,且知道重合部分的大小,我们就知道拼接后的长度,然后我们拿当前搜索的字符串(末尾的字符串)继续和下一个字符串去判断能否合并即可,也就是意味着我们并不需要把他们连起来就可以知道最终长度是多少

问题二:为何要vis[i]–;
因为起始位点可能有多个,当我们从一个可行的起始位点往下搜索完毕后,还要可能存在另外一个可行的起始位点,当用另外一个位点时,之前的位点作为起始位点的 1次 使用次数要消掉

第三步:DFS部分

void dfs(string x, int s)
{
    //求最长长度
    ans = max(ans, s);
    //枚举每一个字符串 
    for (int i = 1; i <= n; i++) {
        int p = 1;//重叠部分长度 
        int la = x.length();
        int lb = a[i].length();
        while (p < min(la, lb)) {
            if (x.substr(la - p) == a[i].substr(0, p) && vis[i] < 2) {
                vis[i]++;
                dfs(a[i], s + lb - p);
                vis[i]--;
            }
            p++;
        }
    }
}
ans = max(ans, s);

在搜索🔍过程中 s 是不断变化的,我们用取大函数将s的最大值赋给一个全局变量ans,ans也是本题的最终答案

int p = 1;//重叠部分长度 

我们想让字符串最长,所以最理想的情况下是重叠部分长度为 1,且每次字符串连接的尝试都要使 p为1所以p定义在for循环内

while (p < min(la, lb))

确保两个字符串没有包含关系

 if (x.substr(la - p) == a[i].substr(0, p) && vis[i] < 2) {
                vis[i]++;
                dfs(a[i], s + lb - p);
                vis[i]--;
                break;
            }
            p++;

判断若重合长度为 p 时是否可以连接
能连接则马上连接(确保p最小)
然后更新使用次数再去连接下一个

用当前被连接的字符串 作为下一步要连接字符串的新的起始位点 并且更新长度 所以 dfs(a[i], s + lb - p);
若不能连接
则加大 p 的长度再次判断,直到跳出while循环


关于该DFS的回溯详解(important)

若没有return编译器自动补全
当我们拼完了若干个字符串后不能够再继续拼接时
while循环跳出,for循环跳出,跑到了return的部分
return回到上一层函数的内部的dfs函数结尾
紧接着执行vis[i]–;收回末端拼接的字符串然后跳出while不再p++
(若不加break,会又进行一次连接(eg:pcc和ccq),虽然也可以连接但是并不是最长的,最终会被舍去,所以不如提前规避掉,提高程序运行效率)
然后继续for循环看看能不能放入一个新的字符串到已经拼接过的字符串中去,若可以则生成新的字符串,若不行则结束for循环return到再上一层函数中去,然后重复相同的过程

其实和全排列问题差不多,一个是放数字,一个是放字符串只是再放字符串的同时判断了前后两个字符串的连接性,都遍历了所有可能性,函数递归的步骤完全相同

问题:为什么没有 if 递归终止条件?

因为往下递到头后for循环结束后会遇到return进行归的过程 全排列问题里为了输出所以放置了 if 递归终止条件,然后再在if内return,但是该题不能用if,直接fou循环后加return即可,因为if的条件语句,我们无法找到 我们是遍历所有的可能性(if没必要) 不像全排列一样,在搜索到最底部时需要输出

问题: if 递归终止条件的作用

void的dfs函数无论有没有if,dfs函数终会回溯(void本不需要return) if 只是为了达到题目要求的功能而已 eg:输出数组

  • 8
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值