7.创意标题匹配问题 | 豆包MarsCode AI 刷题

107 阅读11分钟

题目解析

在广告投放平台中,为了提高广告创意的灵活性和效率,允许广告主在创建广告标题时使用通配符(wildcards)。这些通配符以成对的花括号 {} 括起来,内部可以包含零个或多个字符。在实际投放广告时,系统会根据用户的搜索词替换这些通配符,以生成最终展示给用户的广告标题。这种机制不仅提高了广告创意的多样性,还能根据用户的具体需求动态调整广告内容,从而提升广告的点击率和转化率。

然而,为了确保系统的正确性和高效性,我们需要设计一个算法来判断给定的标题是否可以由特定的模板通过替换通配符生成。这涉及到字符串匹配和模式匹配的知识,尤其是在处理带有通配符的模板时。

问题描述

给定一个包含通配符的模板字符串和若干个标题,判断每个标题是否可以通过替换模板中的通配符生成。通配符以 {} 括起来,内部的内容在匹配过程中被视为任意字符串(包括空字符串)。具体来说,模板中的每一个通配符可以替换为任意长度的字符串(包括空字符串),而模板中的其他部分必须与标题中的相应部分完全匹配。

示例说明:

以样例1为例:

  • 模板"ad{xyz}cdc{y}f{x}e"
  • 标题列表["adcdcefdfeffe", "adcdcefdfeff", "dcdcefdfeffe", "adcdcfe"]

通过分析,只有 "adcdcefdfeffe""adcdcfe" 可以通过替换通配符生成,其他标题则不符合模板的结构。

解题思路

要解决这个问题,我们需要将模板解析为一系列的“部分”(parts),这些部分可以是固定的字面量字符串,也可以是通配符。然后,对于每个标题,我们需要检查它是否可以通过匹配这些部分来生成。

具体步骤如下:

  1. 模板解析

    • 遍历模板字符串,将其分解为若干部分。固定的字面量部分直接记录,而通配符部分则标记为可以匹配任意字符串的占位符。
    • 例如,模板 "ad{xyz}cdc{y}f{x}e" 可以解析为:"ad"(字面量)、{xyz}(通配符)、"cdc"(字面量)、{y}(通配符)、"f"(字面量)、{x}(通配符)、"e"(字面量)。
  2. 匹配机制

    • 对于每个标题,我们需要检查是否存在一种替换方式,使得模板中的通配符可以被替换为某些字符串,使整个模板与标题完全匹配。

    • 具体来说,我们可以采用递归的方式,逐步匹配模板的每一部分与标题的对应部分:

      • 字面量部分:必须在标题的相应位置出现完全相同的字符串。
      • 通配符部分:可以匹配任意长度的字符串,包括空字符串。这里需要考虑通配符后面紧跟的字面量部分,以确定通配符可以匹配的最大范围。
  3. 动态规划优化

    • 由于在匹配过程中可能会有大量的重复计算(例如,多个通配符可以匹配相同的字符串片段),我们可以使用动态规划(DP)来优化匹配过程。
    • 定义一个二维的DP表 dp[i][j],表示模板的第 i 部分与标题的第 j 个字符开始的子串是否可以匹配。
    • 通过递归和记忆化搜索(Memoization),避免重复计算,提高算法效率。
  4. 边界条件和特殊情况处理

    • 模板开头或结尾可能是通配符,这需要特别处理。例如,结尾的通配符可以匹配标题剩余的所有字符。
    • 模板中连续的多个通配符也需要正确处理,确保每个通配符都能独立地匹配相应的字符串片段。
    • 标题可能为空字符串,此时只有模板中的所有部分都能通过通配符匹配空字符串时,才是有效匹配。

算法实现

基于上述思路,算法的主要步骤可以分为以下几个部分:

  1. 模板解析

    • 遍历模板字符串,识别出字面量部分和通配符部分。
    • 使用一个结构体 Part 来表示每一部分,包含内容字符串和是否为通配符的标志。
  2. 匹配函数设计

    • 对于每个标题,调用一个匹配函数 match_title,传入标题字符串和解析后的模板部分。
    • 使用递归和动态规划的方式,实现高效的匹配过程。
  3. 结果收集与输出

    • 对于每个标题,记录其是否匹配模板的结果。
    • 最终,将所有结果按照要求以逗号分隔的字符串形式输出。

复杂度分析

  • 时间复杂度

    • 模板解析的时间复杂度为 O(m),其中 m 是模板字符串的长度。
    • 对于每个标题,匹配的时间复杂度主要取决于标题的长度和模板中的通配符数量。由于使用了动态规划优化,单个标题的匹配时间复杂度接近 O(m * n),其中 n 是标题的长度。
    • 总体时间复杂度为 O(m + k * m * n),其中 k 是标题的数量。
  • 空间复杂度

    • 模板解析需要 O(m) 的空间。
    • 动态规划表需要 O(m * n) 的空间。
    • 总体空间复杂度为 O(m * n)。

示例分析

以样例1为例:

  • 模板"ad{xyz}cdc{y}f{x}e"
  • 标题列表["adcdcefdfeffe", "adcdcefdfeff", "dcdcefdfeffe", "adcdcfe"]

解析过程

  1. 解析模板,得到如下部分:

    • "ad"(字面量)
    • {xyz}(通配符)
    • "cdc"(字面量)
    • {y}(通配符)
    • "f"(字面量)
    • {x}(通配符)
    • "e"(字面量)

匹配过程

  1. 标题1"adcdcefdfeffe"

    • "ad" 匹配 "ad"
    • {xyz} 匹配空字符串
    • "cdc" 匹配 "cdc"
    • {y} 匹配 "e"
    • "f" 匹配 "f"
    • {x} 匹配 "ffe"
    • "e" 匹配 "e"
    • 匹配成功,返回 True
  2. 标题2"adcdcefdfeff"

    • 最后一个 "e" 无法匹配,匹配失败,返回 False
  3. 标题3"dcdcefdfeffe"

    • 开头的 "ad" 无法匹配,匹配失败,返回 False
  4. 标题4"adcdcfe"

    • {xyz} 匹配空字符串
    • {y} 匹配空字符串
    • {x} 匹配空字符串
    • 所有部分匹配,返回 True

边界情况处理

在实际应用中,可能会遇到各种边界情况,例如:

  1. 模板全为通配符

    • 模板如 "{a}{b}{c}",可以匹配任何字符串,包括空字符串。
  2. 标题为空字符串

    • 只有当模板中的所有部分都是通配符时,空字符串才能匹配。
  3. 模板没有通配符

    • 这种情况下,只有与模板完全相同的标题才会匹配。
  4. 连续通配符

    • 模板如 "a{{b}}c",需要正确解析为两个独立的通配符,并分别匹配对应的字符串部分。
  5. 嵌套或不匹配的花括号

    • 本题假设模板是格式良好的,即所有花括号都是成对出现的,没有嵌套。若存在不匹配的花括号,需根据具体要求进行错误处理。

优化与改进

尽管动态规划已经大大提高了匹配的效率,但在某些极端情况下(如模板和标题都非常长,且通配符频繁出现),算法的时间和空间消耗仍可能较高。为此,可以考虑以下优化:

  1. 状态压缩

    • 利用位运算或其他技巧压缩DP表,减少空间占用。
  2. 贪心算法

    • 在某些特定情况下,可以采用贪心策略快速匹配,减少不必要的递归调用。
  3. 提前终止

    • 如果发现某部分无法匹配,可以立即终止当前匹配,避免不必要的计算。
  4. 记忆化搜索优化

    • 使用更高效的数据结构存储和查询已计算的状态,如哈希表。

结论

本题通过解析模板并利用动态规划进行高效的字符串匹配,实现了判断标题是否可以通过模板中的通配符生成的功能。该方法不仅适用于广告投放平台中的创意匹配问题,还具有广泛的应用场景,如文本模板生成、模式匹配等。在实际应用中,可以根据具体需求进一步优化算法,提高其适应性和效率。

参考代码

以下是基于C++11标准的参考实现:

#include <bits/stdc++.h>
using namespace std;

// 函数用于判断标题是否符合模板
std::string solution(int n, std::string template_, 
                    std::vector<std::string> titles) {
    struct Part {
        string text;
        bool is_wildcard;
    };
    
    // 解析模板为部分:字面量和通配符
    vector<Part> parts;
    string current_literal = "";
    bool inside_wildcard = false;
    for(char c : template_){
        if(c == '{'){
            if(!inside_wildcard){
                if(!current_literal.empty()){
                    parts.push_back(Part{current_literal, false});
                    current_literal = "";
                }
                inside_wildcard = true;
            }
            // 假设模板格式正确,无嵌套或未闭合的花括号
        }
        else if(c == '}'){
            if(inside_wildcard){
                parts.push_back(Part{"", true});
                inside_wildcard = false;
            }
            // 假设模板格式正确
        }
        else{
            if(inside_wildcard){
                // 通配符内部的字符被忽略,通配符作为任意字符串的占位符
            }
            else{
                current_literal += c;
            }
        }
    }
    if(!current_literal.empty()){
        parts.push_back(Part{current_literal, false});
    }
    
    // 匹配单个标题的函数
    auto match_title = [&](const string& title, const vector<Part>& parts) -> bool {
        int m = parts.size();
        int n_title = title.length();
        
        // memo[i][j] 表示 parts 从 i 开始和 title 从 j 开始是否匹配
        vector<vector<int>> memo_table(m+1, vector<int>(n_title+1, -1));
        
        // 递归函数,带记忆化搜索
        function<bool(int, int)> dp = [&](int i, int j) -> bool {
            if(memo_table[i][j] != -1){
                return memo_table[i][j] == 1;
            }
            if(i == m){
                memo_table[i][j] = (j == n_title) ? 1 : 0;
                return memo_table[i][j] == 1;
            }
            if(parts[i].is_wildcard){
                if(i == m-1){
                    // 最后一个通配符,可以匹配剩余所有字符
                    memo_table[i][j] = 1;
                    return true;
                }
                else{
                    // 下一个部分必须是字面量
                    string next_literal = parts[i+1].text;
                    // 在 title 中从 j 开始查找 next_literal 的所有位置
                    size_t pos = j;
                    while((pos = title.find(next_literal, pos)) != string::npos){
                        if(dp(i+2, pos + next_literal.length())){
                            memo_table[i][j] = 1;
                            return true;
                        }
                        pos += 1; // 继续查找下一个匹配位置
                    }
                    // 未找到下一个字面量的匹配
                    memo_table[i][j] = 0;
                    return false;
                }
            }
            else{
                // 当前部分是字面量,必须精确匹配
                string literal = parts[i].text;
                if(j + literal.length() > n_title){
                    memo_table[i][j] = 0;
                    return false;
                }
                if(title.compare(j, literal.length(), literal) == 0){
                    bool res = dp(i+1, j + literal.length());
                    memo_table[i][j] = res ? 1 : 0;
                    return res;
                }
                else{
                    memo_table[i][j] = 0;
                    return false;
                }
            }
        };
        
        return dp(0, 0);
    };
    
    // 处理每个标题,收集匹配结果
    vector<string> results;
    for(int i = 0; i < n; ++i){
        bool res = match_title(titles[i], parts);
        results.push_back(res ? "True" : "False");
    }
    
    // 将结果连接为逗号分隔的字符串
    string output = "";
    for(int i = 0; i < results.size(); ++i){
        if(i > 0){
            output += ",";
        }
        output += results[i];
    }
    return output;
}

// 示例使用
int main(){
    // 样例1
    int n1 = 4;
    string template1 = "ad{xyz}cdc{y}f{x}e";
    vector<string> titles1 = {"adcdcefdfeffe", "adcdcefdfeff", "dcdcefdfeffe", "adcdcfe"};
    cout << solution(n1, template1, titles1) << endl; // 输出: True,False,False,True
    
    // 样例2
    int n2 = 3;
    string template2 = "a{bdc}efg";
    vector<string> titles2 = {"abcdefg", "abefg", "efg"};
    cout << solution(n2, template2, titles2) << endl; // 输出: True,True,False
    
    // 样例3
    int n3 = 5;
    string template3 = "{abc}xyz{def}";
    vector<string> titles3 = {"xyzdef", "abcdef", "abxyzdef", "xyz", "abxyz"};
    cout << solution(n3, template3, titles3) << endl; // 输出: True,False,True,True,True
}

代码说明

  1. 结构体 Part

    • 用于表示模板中的每一部分,包括字面量字符串和通配符。
    • text:存储字面量部分的字符串内容;对于通配符部分,text 可以为空。
    • is_wildcard:布尔值,指示该部分是否为通配符。
  2. 模板解析

    • 遍历模板字符串,识别出字面量和通配符部分。
    • 当遇到 { 时,开始标记为通配符部分,并忽略其中的内容。
    • 当遇到 } 时,结束通配符标记,并将其作为一个独立的通配符部分加入 parts 列表。
    • 字符串中非通配符的部分被累积到 current_literal 中,直到遇到通配符或模板结束。
  3. 匹配函数 match_title

    • 接受一个标题和解析后的模板部分,返回标题是否匹配模板。
    • 使用动态规划表 memo_table 记录中间结果,避免重复计算。
    • 递归函数 dp(i, j) 判断模板从第 i 部分和标题从第 j 个字符开始的子串是否匹配。
    • 对于通配符部分,如果是最后一个部分,则直接匹配剩余所有字符;否则,需要查找下一个字面量部分在标题中的所有可能匹配位置,并递归判断剩余部分是否匹配。
    • 对于字面量部分,必须精确匹配标题中的对应子串。
  4. 结果收集与输出

    • 对每个标题调用 match_title 函数,记录匹配结果为 "True""False"
    • 最终将所有结果以逗号分隔的字符串形式返回。