【题解】洛谷P6793 [SNOI2020] 字符串

本文介绍如何使用SAM算法解决字符串子串一致性问题,通过构建后缀链接树并匹配子串,找到最长公共后缀,以最少成本使两个字符串集合相等。关键步骤包括反串建立SAM树、拓扑排序和后缀匹配。

【题解】P6793 [SNOI2020] 字符串(SAM)


D e s c r i p t i o n \rm Description Description

有两个长度为 n n n 的由小写字母组成的字符串 a , b a,b a,b,取出他们所有长为 k k k 的子串(各有 n − k + 1 n-k+1 nk+1 个),这些子串分别组成集合 A , B A,B A,B 。现在要修改 A A A 中的串,使得 A A A B B B 完全相同。可以任意次选择修改 A A A 中一个串的一段后缀,花费为这段后缀的长度。总花费为每次修改花费之和,求总花费的最小值。

输入格式
第一行两个整数 n , k n,k n,k 表示字符串长度和子串长度;
第二行一个小写字母字符串 a a a
第三行一个小写字母字符串 b b b

输出格式
输出一行一个整数表示总花费的最小值。

输入输出样例
输入

5 3
aabaa
ababa

输出

3

对于所有数据, 1 ≤ k ≤ n ≤ 1.5 × 1 0 5 1≤k≤n≤1.5×10^5 1kn1.5×105


S o l u t i o n \rm Solution Solution

由于修改的是子串的后缀,也就是前缀是相同的,问题可以转化为对集合 A A A B B B 的子串进行匹配,使其公共前缀的长度和最大,即 ∑ i = 1 n l c p ( A i , B i ) \sum\limits_{i=1}^n lcp(A_i,B_i) i=1nlcp(Ai,Bi) 最大。

S A M \rm SAM SAM只能处理后缀,可以用反串建立 S A M \rm SAM SAM处理。
两个子串的最长公共后缀等于其在 p a r e n t \rm parent parent t r e e \rm tree tree (也有人称为后缀链接)上的最近公共祖先( L C A \rm LCA LCA )。
为了方便处理,将两个字符串 a , b a,b a,b 拼起来,记为 s s s ,用 s s s 的反串建立 S A M \rm SAM SAM ,再在 p a r e n t \rm parent parent t r e e \rm tree tree 上区分 a , b a,b a,b 进行匹配。

s u m sum sum 为当前公共前缀的长度和,当匹配至节点 i i i 时,有 s 1 i s1_i s1i a a a l e n len len大于等于 k k k 的子串(由于求的是最长公共后缀,所以 l e n len len 不需要等于 k k k)、 有 s 2 i s2_i s2i b b b l e n len len 大于等于 k k k 的子串没有在 i i i 的儿子节点被匹配,有 m i n ( s 1 i , s 2 i ) min(s1_i,s2_i) min(s1i,s2i) a , b a,b a,b 子串的 L C A \rm LCA LCA 为节点 i i i ,更新 s u m sum sum ,剩下的部分则上传到父亲节点继续匹配。

答案为长度为 k k k 的子串个数减去最大的公共前缀长度和,即 ( k ∗ ( n − k + 1 ) − s u m (k*(n-k+1) -sum (k(nk+1)sum


C o d e \rm Code Code

#include<bits/stdc++.h>
using namespace std;
long long n,k;
int t[1000005],a[1000005];
char s[500005];
struct node{
	int nex[26];
	int fa;
	long long len,sum[2];
	node(){memset(nex,0,sizeof(nex)); len=0;}
}d[1000005];
int tot=1,las=1;
long long ans;
void add(int c,int val,int jud)
{
	int p=las,np=las=++tot; d[np].sum[jud]=val;
	d[np].len=d[p].len+1;
	for(; p && !d[p].nex[c]; p=d[p].fa) d[p].nex[c]=np;
	if(!p) d[np].fa=1;
	else
	{
		int q=d[p].nex[c];
		if(d[q].len == d[p].len+1) d[np].fa=q;
		else
		{
			int nq=++tot; 
			memcpy(d[nq].nex,d[q].nex,sizeof(d[nq].nex));
			d[nq].fa=d[q].fa;
			d[nq].len=d[p].len+1;
			d[q].fa=d[np].fa=nq;
			for(; p && d[p].nex[c]==q; p=d[p].fa) d[p].nex[c]=nq;
		}
	}
}
void tsort() //拓扑排序
{
	for(int i=1; i<=tot; i++) t[d[i].len]++;
	for(int i=1; i<=tot; i++) t[i]+=t[i-1];
	for(int i=1; i<=tot; i++) a[t[d[i].len]--]=i;
}
int main()
{
	scanf("%lld%lld",&n,&k);
	scanf("%s",s);
	for(int i=n-1; i>=0; i--) add(s[i]-'a',(i+k-1<n),0);
	scanf("%s",s);
	for(int i=n-1; i>=0; i--) add(s[i]-'a',(i+k-1<n),1);
	int lens=n<<1;
	tsort();
	int mins;
	for(int i=tot; i; i--)
	{
		mins=min(d[a[i]].sum[0],d[a[i]].sum[1]);
		ans+=mins*min(k,d[a[i]].len);
		d[a[i]].sum[0]-=mins;
		d[a[i]].sum[1]-=mins;
		d[d[a[i]].fa].sum[0]+=d[a[i]].sum[0];
		d[d[a[i]].fa].sum[1]+=d[a[i]].sum[1];
	}
	ans=k*(n-k+1)-ans;
	printf("%lld",ans);
	return 0;
} 



感谢阅读,如果有问题或更好的建议,还请提出。

洛谷 P2516 题目涉及的是一个与字符串处理和动态规划相关的挑战。题目要求对一个由数字组成的字符串进行分割,使得每个分割出的数字子串能构成一个递增序列,并且每部分对应的数值都比前一部分大。以下是解题思路及实现方法。 ### 问题解析 - 输入是一个长度不超过 **40** 的纯数字字符串。 - 目标是将该字符串分割成若干个非空数字子串,这些子串所表示的数值形成一个严格递增序列。 - 每个分割出来的子串必须满足其数值大于前一个子串的数值。 - 最终输出所有可能的合法分割方案的数量。 ### 解法概述 此问题可以通过 **深度优先搜索 (DFS)** 或 **回溯法** 来解决: - 使用递归的方式尝试在每一个位置进行分割。 - 对于每一次分割,提取当前子串并转换为整数,然后判断它是否大于上一次分割得到的值。 - 如果符合递增条件,则继续递归处理剩余的字符串部分。 - 当遍历完整个字符串并且满足所有分割条件时,计数器加一。 ### 实现细节 - 因为输入字符串长度最大为 **40**,所以需要考虑大数问题(超过 `int` 范围),建议使用 `long long` 类型或 Python 中的 `int` 类型自动处理大整数。 - 在分割过程中,确保没有前导零(除非子串长度为 1)。 - 递归终止条件是字符串已经被完全分割。 ### 示例代码 (C++) ```cpp #include <iostream> #include <string> using namespace std; int count = 0; // 将字符串 s 的 [start, end) 子串转换为整数 long long to_number(const string &s, int start, int end) { long long num = 0; for (int i = start; i < end; ++i) { num = num * 10 + (s[i] - '0'); } return num; } // DFS 函数:从 pos 位置开始分割,last_num 表示上一次分割出的数 void dfs(const string &s, int pos, long long last_num) { if (pos == s.size()) { count++; return; } for (int i = pos + 1; i <= s.size(); ++i) { // 剪枝:如果子串长度大于1且以0开头,则跳过 if (i - pos > 1 && s[pos] == '0') break; long long current = to_number(s, pos, i); if (current > last_num) { dfs(s, i, current); } } } int main() { string s; cin >> s; dfs(s, 0, -1); // 初始时 last_num 设置为 -1,保证第一个数可以任意选择 cout << count << endl; return 0; } ``` ### 算法复杂度分析 - 时间复杂度:最坏情况下为指数级 $O(2^n)$,因为每次递归都有多个分支。 - 空间复杂度:主要取决于递归栈深度,最多为 $O(n)$。 这种方法适用于题目给定的数据规模(字符串长度 ≤ 40),通过适当的剪枝优化,可以在合理时间内完成计算。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值