1 条题解

  • 0
    @ 2025-10-19 14:23:43

    这里是人肉爬虫114514号awa,这是我爬出的第2个题目,以下是由生物计算机(已迭代20次)生成的内容:

    知识点:

    • KMP算法: 高效的字符串匹配算法,要解决的是这样一个经典问题:在一个主文本字符串 text 中,查找一个模式字符串 pattern 是否出现,以及出现的位置
    • KMP 算法的核心在于:利用next数组,来决定模式串在匹配失败后应该向后滑动多少位,而不是仅仅一位。
    • 当在模式串的第 j 个位置匹配失败时,说明模式串的前 j-1 个字符(P0到Pj-2)已经和文本串对应位置的字符匹配成功了。那么,我们可以找到已匹配成功的那段子串(P0到Pj-2)中,最长公共前后缀
    • 前缀:指除了最后一个字符以外,一个字符串的全部头部组合。
    • 后缀:指除了第一个字符以外,一个字符串的全部尾部组合。
    • 最长公共前后缀(LPS):一个字符串中,最长的那个既是前缀又是后缀的子串的长度。
    • eg:子串 "ABAB":前缀 ["A","AB","ABA"],后缀 ["B","AB","BAB"],公共是 "AB",长度为 2。 然后得出next[4]=2,即当第5位(从0开始,j=4)与text[i]不匹配时,i不变,j从2开始(匹配时i++,j++)。
    • 如果从0开始,则设next[0]=-1/0;从1开始设next[1]=-1/0;当next[j]=-1/0时,i++,j=0;其它情况则如例子一样,i不变j变

    主要思想

    • 1.通过KMP算出所有字符串后接所有字符串所产生的代价;
    • 2.定义矩阵A(0,n)为以字符串Sn+1结尾的最小长度,其初始化长度为字符串Sn+1的长度; 再定义B(m,n)为字符串m+1后添加字符串n+1所产生的代价,其初始值为第一步算出的值。
    • 3(最难).定义矩阵乘法*运算和矩阵幂^运算,具体情况看代码中的注释。

    免责声明

    • 我只是个在三友行刷了400题左右的小卡拉米,这道题对于我来说真超标了。
    • 我之前刷到过动态规划的题,但我真的很难理解它(前缀和和差分我都理解了),所以我对于此类题的态度是放弃。但现在完成动态规划的题是我的任务,我只能硬着头皮去理解了。
    • 总而言之,算了,去看题解吧。

    题解(复制到编译器里看)

    #include <bits/stdc++.h>
    using namespace std;
    typedef long long ll;
    const int N = 205;
    const ll inf = 1e18;
    
    int n[N], m, k;  // n[i]: 第i个字符串的长度; m: 字符串数量; k: 需要出现的最小次数
    ll as = inf;     // 存储最终答案
    string a[N];     // 存储所有字符串
    vector<int> nxt[N];  // KMP算法的next数组
    int d[N][N];  // d[i][j]: 在字符串i后面接上字符串j时,需要额外添加的字符数
    
    // KMP预处理函数,为第x个字符串计算其next数组nxt[x][i] 
    void KMP(int x) 
    {
        nxt[x].resize(n[x] + 1, 0);  // 初始化next数组
    
        // 标准的KMP算法next数组计算过程
        for (int i = 2, j = 0; i <= n[x]; ++i) {
            while (j && a[x][i] != a[x][j + 1])
                j = nxt[x][j];  // 失配时回退
    
            if (a[x][i] == a[x][j + 1])
                ++j;  // 匹配成功,j前进
    
            nxt[x][i] = j;  // 记录next值
        }
    }
    
    
    
    // 计算两个字符串之间的重叠关系
    int mt(int x, int y)
    {
        // 使用KMP算法计算字符串x的后缀与字符串y的前缀的最大匹配长度(要在x后面接上y) 
        
        //KMP放在for循环中可读性确实差 
        
        for (int i = 1, j = 0; i <= n[x]; ++i) 
    	{
            while (j && a[x][i] != a[y][j + 1])//j=0时,i++;j!=0且不相等时,i不变j变 
                j = nxt[y][j];  // 失配时回退
    
            if (a[x][i] == a[y][j + 1])
                ++j;  // 匹配成功,j前进
    
            // 如果已经匹配到字符串x的末尾,返回当前匹配长度
            if (i == n[x])
                return j;
                
            // 如果完全匹配了字符串y,回退到next位置继续匹配(因为不是求y在不在x,而是求y的前缀与x的后缀的重叠长度)
            if (j == n[y])
                j = nxt[y][j];
        }
    
        return -1;  // 理论上不会执行到这里
    }
    
    // 矩阵类,用于快速幂优化动态规划,这是这道题最难的地方 
    struct mat {
        ll a[N][N];  // 矩阵数据
        
        // 构造函数,初始化矩阵为无穷大
        mat() {
            for (int i = 0; i < m; ++i)
                for (int j = 0; j < m; ++j)
                    a[i][j] = inf;
        }
        
        // 定义了一个特殊的矩阵乘法(最难)
    // A = [……](字符串为?个,表示分别以s1,s2,s3……结尾的长度最小值) 
    
    //如何得到以s1为结尾的长度最小值呢?你只需要在每个字符串后面都试着加上s1就行了。但出现次数的最大值来到了恐怖的10^9次方,这对想法的实现要求非常高。 
    //设A1=[s1.len,s2.len,s3.l3n……] 
    
    //假如有两个字符串,那么可以通过A1计算 s1.len+(s1到s1的代价),s2.len+(s2到s1的代价)……取其最小得到A2[SS1.len,……] 
    
    //假如有三个字符串,那么可以通过A2计算SS1.len+(s1到s1的代价),SS2.len+(s2到s1的代价)……取其最小得到A3 
    //…… 
    
    	//假设有3个字符串:
    	//s1: "ab" (长度2)
    	//s2: "bc" (长度2)
    	//s3: "cd" (长度2)
    //得出	A = [2, 2, 2](字符串为一个,表示分别以s1,s2,s3结尾的长度最小值)
    
    
    //B = [2, 1, 2]  // 第0行: s1→s1, s1→s2, s1→s3
    //    [2, 2, 1]  // 第1行: s2→s1, s2→s2, s2→s3  
    //    [2, 2, 2]  // 第2行: s3→s1, s3→s2, s3→s3
    
    //      A*B = [4, 3, 3](字符串为两个,表示分别以s1,s2,s3结尾的长度最小值)
    //      A*B = [C(0,0),C(0,1),C(0,2)];
    		//k=0: A[0][0] + B[0][0] = 2 + 2 = 4  // s1→s1
    		//k=1: A[0][1] + B[1][0] = 2 + 2 = 4  // s2→s1  
    		//k=2: A[0][2] + B[2][0] = 2 + 2 = 4  // s3→s1
    		//C[0][0] = min(4, 4, 4) = 4;C[0][1]同理 在所有可能字符串后加上s2 
     
    //		B*B = [3, 3, 2]   //第0行:s1→?→s1 的最小代价是3,s1→?→s2 的最小代价是3,s1→?→s3 的最小代价是2 
    //     		 [3, 3, 3]
    //     		 [4, 3, 3]
    //第1轮:i=0, j=0(s1→s1)
    // k=0: C[0][0] = min(inf, B[0][0] + B[0][0]) = min(inf, 2+2) = 4 (1→1→1)
    // k=1: C[0][0] = min(4, B[0][1] + B[1][0]) = min(4, 1+2) = 3  (1→2→1)
    // k=2: C[0][0] = min(3, B[0][2] + B[2][0]) = min(3, 2+2) = 3  (1→3→1)
    //C[0][0] = 3 即 s1→?→s1 的最小代价是3
    //C[1][0] = 3 即 s2→?→s1 的最小代价是3
    //C[2][0] = 4 即 s3→?→s1 的最小代价是3
    
    //不应该理会B*B的具体含义,因为原理中是A*B*B,只是实现中是A*(B^2),如果要理解的话还是得看原理 
    //A:只有一个字符串,是所有字符串的集合;
    //A*B:两个字符串,通过对A中每个字符串后加上sn选其长度最小值来当A的第n个字符串 
    //A*B*B:三个字符串,通过对(A*B)中每个字符串后加上sn选其长度最小值来当A的第n个字符串 
        mat operator * (const mat &t) const {
            mat as;  // 结果矩阵
            
    		//m:字符串数量 
            // 三重循环实现特殊矩阵乘法(A*B),但使用min操作而非加法
            //这不能根据实现来看原理,原理中k在内循环,但因空间局部性,作者把k放在了外循环!!!!
    		//我的天啊, 这都要贪啊,一个月工资多少啊你。 
            for (int k = 0; k < m; ++k)
    		{
                for (int i = 0; i < m; ++i) 
    			{
                    ll tt(a[i][k]);  // 中间值 等价于ll tt = this→a[i][k],即被乘矩阵A(传进来的t是乘矩阵B); 
    
                    for (int j = 0; j < m; ++j) 
                        as.a[i][j] = min(as.a[i][j], tt + t.a[k][j]); 
                        //                           ↑A 
                }
            }
    
            return as;
        }
        
        // 矩阵快速幂
        mat operator ^ (int b) {
            mat as, base;  // as: 单位矩阵,base: 底数矩阵
    
            // 初始化单位矩阵(对角线为0,其他为inf)
            for (int i = 0; i < m; ++i)
                as.a[i][i] = 0;
    
            // 复制当前矩阵(B)到底数矩阵
            for (int i = 0; i < m; ++i)
                for (int j = 0; j < m; ++j)
                    base.a[i][j] = a[i][j];
    
            // 快速幂算法(B*B*B……)
            while (b) 
    		{
                if (b & 1)//b&1指b是奇数(最低位为1) 这是个很美丽的数学公式,本来as也要像base一样累乘的,写这段代码的人肯定是个大佬。 
                    as = as * base;  // 当前位为1时乘到结果中
    
                base = base * base;  // 底数平方
                b >>= 1;  // 右移一位
            }
    
            return as;
        }
    };
    
    int main() {
        ios::sync_with_stdio(false);
    	cin.tie(0);cout.tie(0);
    	
        cin >> m >> k;  // 读入字符串数量和需要出现的最小次数
    	
        // 读入所有字符串并预处理
        for (int i = 1; i <= m; ++i) 
    	{
            cin >> a[i];  // 读入字符串
            n[i] = a[i].size();  // 记录长度
            a[i] = " " + a[i];  // 在字符串前添加空格,使得下标从1开始
            KMP(i);  // 计算KMP(见知识点) next数组(数据结构)
        }
    
        // 计算转移矩阵d
        // d[i][j]表示在字符串i后面接上字符串j时,需要额外添加的字符数。eg:ab+bc 只需要加上c即可 
        for (int i = 1; i <= m; ++i)
            for (int j = 1; j <= m; ++j)
                d[i][j] = n[j] - (i == j ? nxt[i][n[i]] : mt(i, j));
    //                                     ↑表示字符串与自己的最长公共前后缀长度 abab 在后面链接 abab 只需要再加个 ab 即可 
    	// mt(i, j):计算 字符串i的后缀 与 字符串j的前缀 的最大公共长度 
    
    	//上面的还算好理解,就是个KMP算法的应用题,下面的就有点难了。	
    	
        // 动态规划过程,使用矩阵快速幂优化(没错,又是DP)
        
        // 状态定义:f[i][u]表示使用i个字符串,以字符串u结尾时的最小长度
        // 转移方程:f[i][u] + d[u][v] -> f[i+1][v] (容易理解)
    	// 本来用f已经可以做出来这题了,但作者为了降低时复,用了矩阵快速幂 
    	 
        mat A, B;  // A: 初始状态矩阵,B: 转移矩阵
    	
        // 初始化转移矩阵B(就是把d换个下标和数据类型) 
        for (int i = 0; i < m; ++i)
            for (int j = 0; j < m; ++j)
                B.a[i][j] = d[i + 1][j + 1];  // 注意下标映射,d从1开始 
    
        // 初始化初始状态矩阵A
        // 一开始,A.a[0][i]表示以第i个字符串开头时的初始长度
        for (int i = 0; i < m; ++i)
            A.a[0][i] = n[i + 1];//容易理解 
    
        // 使用矩阵快速幂计算经过k-1次转移后的状态(跳转到mat类查看* ^的定义)
        A = A * (B ^ (k - 1));
    	//k:需要出现的次数 
    	
    	//我们要构造包含 k 个字符串出现的优美字符串
    	//需要从初始状态进行 k-1 次转移:B ^ (k - 1)
    	
    	
        // 在所有可能的最终状态中找最小值
        for (int i = 0; i < m; ++i)
            as = min(as, A.a[0][i]);
    	//最后,A.a[0][j]表示:包含 k 个字符串出现的优美字符串以第j个字符串作为最后一个出现的最小长度
    	
    	
        cout << as;  // 输出答案
    }
    //你猜我得出“这不能根据实现来看原理,原理中k在内循环,但因空间局部性,作者把k放在了外循环!!!!” 这个结论花了多少时间?T_T 
    
    • 1

    信息

    ID
    3297
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    2
    已通过
    1
    上传者