题目描述
难度分: 2300
输入长度≤5000的字符串s,只包含a
和b
。
设t是s的一个连续子串,长为m,下标从1开始。如果对于[1,⌈m2⌉]中的所有奇数下标i,都满足t[i]=t[m−i+1],那么称t为半回文串。
然后输入正整数k。
输出s的字典序第k小的半回文子串。保证k不超过s的半回文子串的个数。
输入样例1
abbabaab
7
输出样例1
abaa
输入样例2
aaaaa
10
输出样例2
aaa
输入样例3
bbaabb
13
输出样例3
bbaabb
算法
一周灵茶AK时刻!
动态规划+字典树
首先我们要知道哪些子串[l,r]是半回文的,然后确定字典序排名第k的是哪个。
状态定义
f[l][r]表示子串[l,r]是否是半回文串。
状态转移
按照区间DP
的套路,外层循环遍历长度len∈[2,n](初始化f[i][i]=true
,i∈[1,n])。内层循环遍历右端点r∈[len,n],那么左端点为l=r−len+1。接下来状态转移在s[l]=s[r]成立时有两种情况:
- len是奇数,当len=3时f[l][r]=
true
;当len>3时,[l,r]是否半回文取决于[l+2,r−2]是否为半回文,状态转移方程为f[l][r]=f[l+2][r−2]。 - len是偶数,当len≤4时f[l][r]=
true
;当len>4时,[l,r]是否半回文取决于[l+2,r−2]是否为半回文,状态转移方程为f[l][r]=f[l+2][r−2]。
构建一个映射l2r,当l是半回文串的起始字符时,l2r[l]是这些半回文串的右端点列表(按照升序排列)。然后遍历l∈[1,n],按照增量的方式把[l,ri]插入到字典序中,即先插入[l,r1],然后插入[r1+1,r2],……,依次类推,反正只有每个右端点才打单词结尾标记,这样插是没问题的。否则每次都插入[l,ri]的话,子串数量是O(n2),插入字典树的时间复杂度为O(n),就会得到一个O(n3)的操作从而超时。
DFS
遍历字典树
从字典树的根节点开始DFS
,每层节点都先遍历a
再遍历b
,这样就能保证是按照最小字典序在遍历。每遇到一个半回文结尾位置的节点,排名就增加,直到排名覆盖住k,这样就能够构造出排名第k的半回文串,详见代码。
复杂度分析
时间复杂度
区间DP
的时间复杂度为O(n2);预处理出l2r需要双重循环遍历s的所有子串,时间复杂度也为O(n2);接下来将O(n2)级别的本回文串加入到字典树中,对于O(n)个l,都需要遍历O(n)级别的右端点,时间复杂度为O(n),总的时间复杂度为O(n2);最后对字典树进行深度优先遍历,字典树的节点个数为O(n2Σ),本题s串只包含a
和b
两种字符,因此字符集大小Σ=2,时间复杂度为O(n2)。
综上,整个算法的时间复杂度为O(n2)。
空间复杂度
DP
矩阵f、映射l2r,以及字典树的空间复杂度均为O(n2),所以整个算法的额外空间复杂度为O(n2)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
const int N = 5001;
int n, k, rk;
char s[N];
bool f[N][N];
vector<int> l2r[N];
string ans;
class TrieNode {
public:
int end;
TrieNode* next[2];
TrieNode() {
end = 0;
for(int i = 0; i < 2; i++) {
next[i] = nullptr;
}
}
};
TrieNode* root;
void insert(int l, int r, int d) {
TrieNode* cur = root;
for(int i = l; i <= r; i++) {
int idx = s[i] - 'a';
if(cur->next[idx] == nullptr) {
cur->next[idx] = new TrieNode;
}
cur = cur->next[idx];
}
cur->end += d;
}
bool dfs(TrieNode* node) {
if(node->end > 0) {
k -= node->end;
if(k <= 0) return true;
}
for(int i = 0; i < 2; i++) {
if(node->next[i]) {
ans.push_back('a' + i);
if(dfs(node->next[i])) {
return true;
}
ans.pop_back();
}
}
return false;
}
int main() {
scanf("%s", s + 1);
scanf("%d", &k);
n = strlen(s + 1);
memset(f, 0, sizeof f);
for(int i = 1; i <= n; i++) {
f[i][i] = true;
}
for(int len = 2; len <= n; len++) {
for(int r = len; r <= n; r++) {
int l = r - len + 1;
if(len&1) {
if(s[l] == s[r]) {
if(len == 3) {
f[l][r] = true;
}else {
f[l][r] = f[l + 2][r - 2];
}
}
}else {
if(s[l] == s[r]) {
if(len <= 4) {
f[l][r] = true;
}else {
f[l][r] = f[l + 2][r - 2];
}
}
}
}
}
for(int l = 1; l <= n; l++) {
l2r[l].clear();
for(int r = l; r <= n; r++) {
if(f[l][r]) {
// [l,r]是半回文串,插入到字典树中
l2r[l].push_back(r);
}
}
}
root = new TrieNode;
for(int l = 1; l <= n; l++) {
TrieNode* cur = root;
int pre = l, d = l2r[l].size();
for(int r: l2r[l]) {
for(int i = pre; i <= r; i++) {
int idx = s[i] - 'a';
if(cur->next[idx] == nullptr) {
cur->next[idx] = new TrieNode;
}
cur = cur->next[idx];
}
cur->end++;
pre = r + 1;
}
}
ans.clear();
dfs(root);
cout << ans << endl;
return 0;
}