Codeforces Global Round 7
题目链接:https://codeforces.com/contest/1326
A - Bad Ugly Numbers
题意:求一个n位数,其所有十进制位都非零,其不被其任何一个十进制位整除。
题解:构造一个23、233、2333这样的数字,显然这个数不是2的倍数也不是3的倍数。( 10 mod 3 = 1,所以 p*10^k mod 3 = p mod 3 ,所以就是小学学过的“3的倍数各位数字加起来还是3的倍数”,同理“9的倍数各位数字加起来还是9的倍数” )
B - Maximums
题意:假设有一个序列 \(a\) ,定义一个序列 \(x\) ,为 \(x_i=max(a_1,...,x_{i-1})\) ,特别地, \(x_1=0\) ,再定义一个序列 \(b\) ,满足 \(b_i=a_i-x_i\) ,给出序列 \(b\) ,且 \(b_i\in[-10^9,10^9]\) ,构造序列 \(a\) 。保证存在一个解,构造出的序列 \(a\) 中的数字都在 \([0,10^9]\) 范围内。
题解:一开始还原出序列之后把 \(a\) 全体减去最小值(使得最小值变成0,以为要通过这种方法消除负数) ,WA3,血亏。仔细想想, \(a_1=b_1+x_1=b_1\) 就是一个定值,然后 \(a_2=b_2+x_2=b_2+max(a_1)\) 也是一个定值……整个序列就是一个定值,也就是只有一个唯一解。
C - Permutation Partitions
题意:给出一个n个数的排列,然后规定把这个排列恰好划分成k个非空区间(不重不漏)。每个区间取出最大值,然后求和得到一个sum。有多少种方法使得sum最大,顺便求出sum是多少。
题解:显然sum就是最大的k个数字加起来,也就是每个区间中只含有最大的k个数字中的恰好一个,那么打断区间的位置就是这些数字之间的任意一个位置,统计这个值,然后全部乘一起,就可以了。
int a[200005], pos[200005];
int ans[200005];
void test_case() {
int n, k;
scanf("%d%d", &n, &k);
for(int i = 1; i <= n; ++i) {
scanf("%d", &a[i]);
pos[a[i]] = i;
}
ll res1 = 0;
for(int i = n, j = 1; j <= k; --i, ++j) {
ans[j] = pos[i];
res1 += i;
}
sort(ans + 1, ans + 1 + k);
ll res2 = 1;
for(int j = 2; j <= k; ++j) {
res2 = res2 * (ans[j] - ans[j - 1]);
if(res2 >= MOD)
res2 %= MOD;
}
printf("%lld %lld\n", res1, res2);
return;
}
*D1 - Prefix-Suffix Palindrome (Easy version)
这道题D2不会做,但是D1可以暴力搞,所以这次分开写。
题意:给一个长度不超过5000的字符串s,从这个字符串中选出一个前缀a(可以为空)和一个后缀b(也可以为空),然后求最长的回文字符串a+b的长度,且这个长度不能超过s的长度n。
题解:这个长度不能超过s的长度,意味着前后缀之间不会有重叠的部分,但是在这个算法中并不会影响。假设我们枚举这个长度len从1到n,那么怎么验证存在一个这样的长度的字符串呢?再枚举前缀的长度x从0到n,相应的计算出后缀的长度y=len-x,那么可以从原串中取出这两个部分,拼在一起判断是不是回文。假如再暴力判断则是一个 \(O(n^3)\) 的算法。容易发现判断前后缀接在一起是回文可以变成判断前后缀中左右相同长度的位置是镜像,然后剩下长出来的部分是个回文。判断镜像可以把字符串倒过来然后哈希,判断一个区间是个回文串可以进行假的区间dp。
哈希就没什么好说的了,假的区间dp的话,先预处理长度为1和长度为2的简单情况,然后枚举长度len从3到n,再枚举左端点l,计算出相应的右端点r,就有:
dp[l][r]=(s[l]==s[r])&&dp[l+1][r-1];
char s[1000005];
char r[1000005];
char t[1000005];
ull ha1[1000005];
ull ha2[1000005];
bool isp1[5005][5005];
bool isp2[5005][5005];
void test_case() {
scanf("%s", s + 1);
int n = strlen(s + 1);
memcpy(r, s, sizeof(r[0]) * (n + 2));
reverse(r + 1, r + 1 + n);
for(int i = 1; i <= n; ++i) {
ha1[i] = ha1[i - 1] * BASE + s[i] - 'a';
ha2[i] = ha2[i - 1] * BASE + r[i] - 'a';
}
memset(t, '\0', sizeof(t[0]) * (n + 2));
for(int i = 1; i <= n; ++i) {
for(int j = 1; j <= n; ++j) {
isp1[i][j] = 0;
isp2[i][j] = 0;
}
}
for(int i = 1; i <= n; ++i) {
isp1[i][i] = 1;
isp2[i][i] = 1;
if(i >= 2 && s[i] == s[i - 1])
isp1[i - 1][i] = 1;
if(i >= 2 && r[i] == r[i - 1])
isp2[i - 1][i] = 1;
}
for(int len = 3; len <= n; ++len) {
for(int i = 1; i <= n; ++i) {
int j = i + len - 1;
if(j > n)
break;
if(s[i] == s[j] && isp1[i + 1][j - 1])
isp1[i][j] = 1;
if(r[i] == r[j] && isp2[i + 1][j - 1])
isp2[i][j] = 1;
}
}
int ans = 0;
for(int len = 1; len <= n; ++len) {
for(int x = 0; x <= len; ++x) {
int y = len - x;
int z = min(x, y);
if(ha1[z] == ha2[z]) {
if(x == y) {
ans = len;
for(int j = 1; j <= x; ++j)
t[j] = s[j];
for(int j = 1; j <= y; ++j)
t[j + x] = r[y + 1 - j];
break;
}
if(x > y) {
if(isp1[y + 1][x]) {
ans = len;
for(int j = 1; j <= x; ++j)
t[j] = s[j];
for(int j = 1; j <= y; ++j)
t[j + x] = r[y + 1 - j];
break;
}
} else {
if(isp2[x + 1][y]) {
ans = len;
for(int j = 1; j <= x; ++j)
t[j] = s[j];
for(int j = 1; j <= y; ++j)
t[j + x] = r[y + 1 - j];
break;
}
}
}
}
}
puts(t + 1);
return;
}
*D2 - Prefix-Suffix Palindrome (Hard version)
标签:回文,回文自动机,PAM,manacher
题意:和上一题一样,不过字符串的长度改成1e5。
题解:直接观察可以发现,假如前后缀的两端本身就是镜像的,那么这个部分是一定选的,把这部分贪心掉,剩下一个中间的字符串,满足左右两端不等。剩下的问题就是要选一个最长的回文串,并且这个回文串必须是前缀或者后缀。这不就是pam的定义吗?pam的一个节点占用4*28约128个字节的内存,1e6个节点就只需要大概约128MB的内存。
回忆回文自动机的定义:每个节点存以这个节点为结尾的最长回文(加入一个字符之后,可能可以得到一个新的以这个节点为结尾的本质不同的回文串)。其fail指针跳指向一个继续以这个节点为结尾的短一点的回文。
const int MAXN = 1000000 + 15;
char s[MAXN], t[MAXN];
struct PAM {
int ch[MAXN][26];
int fail[MAXN];
int len[MAXN];
int tot, last;
void Init() {
len[0] = 0;
fail[0] = 1;
len[1] = -1;
fail[1] = 0;
tot = 1;
last = 0;
memset(ch[0], 0, sizeof(ch[0]));
memset(ch[1], 0, sizeof(ch[1]));
}
int NewNode(int l) {
int now = ++tot;
memset(ch[tot], 0, sizeof(ch[tot]));
len[now] = l;
return now;
}
void Extend(int c, int pos) {
int u = last;
while(s[pos - len[u] - 1] != s[pos])
u = fail[u];
if(ch[u][c] == 0) {
int now = NewNode(len[u] + 2);
int v = fail[u];
while(s[pos - len[v] - 1] != s[pos])
v = fail[v];
fail[now] = ch[v][c];
ch[u][c] = now;
}
last = ch[u][c];
}
} pam;
void test_case() {
scanf("%s", t + 1);
int tl = strlen(t + 1);
int l = 1, r = tl;
while(l < r && t[l] == t[r])
++l, --r;
if(l >= r) {
puts(t + 1);
return;
}
int sl = 0;
for(int i = l; i <= r; ++i)
s[++sl] = t[i];
pam.Init();
for(int i = 1; i <= sl; ++i)
pam.Extend(s[i] - 'a', i);
int len1 = pam.len[pam.last];
reverse(s + 1, s + 1 + sl);
pam.Init();
for(int i = 1; i <= sl; ++i)
pam.Extend(s[i] - 'a', i);
int len2 = pam.len[pam.last];
if(len1 >= len2) {
for(int i = 1; i < l; ++i)
putchar(t[i]);
for(int i = 1; i <= len1; ++i)
putchar(s[i]);
for(int i = r + 1; i <= tl; ++i)
putchar(t[i]);
putchar('\n');
} else {
reverse(s + 1, s + 1 + sl);
for(int i = 1; i < l; ++i)
putchar(t[i]);
for(int i = 1; i <= len2; ++i)
putchar(s[i]);
for(int i = r + 1; i <= tl; ++i)
putchar(t[i]);
putchar('\n');
}
}
意思就是,PAM每次执行完Extend之后,last就是最后一个字符对应的以这个字符为结尾的最长回文串所在的节点,last从所在的树的树根走向last时途径的字符串就是这个回文串的一半。详细理解Extend的过程:取出以前一个字符为结尾的最长回文串,判断在这个回文串的前一个字符和新Extend的字符是否相同,若相同则这个就是以新Extend的字符为结尾的最长回文,长度为原来的长度len[u]+2,若这个回文串之前已经出现过,则不再创建。无论是否创建,last都会指向新Extend的字符所在的最长回文串。那么得到这个性质之后,本质不同的回文串的数目,就是PAM中的(非根)节点数。每种回文串的出现次数,在Extend结束的时候加上1,然后记得加上fail树上所有祖先的出现次数(其祖先出现了,自己一定也出现)。每种回文串还可以携带一些奇奇怪怪的信息。
注意PAM的内存消耗是单倍n,与SAM消耗的是双倍n不同。
这道题中需要的是插入整个字符串后,包含最后一个字符的最长回文子串,所以就是len[last]的值。
manacher算法。算出每个中心的最长回文串,然后更新一下。
char s[1000005];
int len[2000005];
void manacher(char *s, int *len, int n) {
len[0] = 1;
for(int i = 1, j = 0; i < (n << 1) - 1; ++i) {
int p = i >> 1, q = i - p, r = ((j + 1) >> 1) + len[j] - 1;
len[i] = r < q ? 0 : min(r - q + 1, len[(j << 1) - i]);
while(p > len[i] - 1 && q + len[i] < n && s[p - len[i]] == s[q + len[i]])
++len[i];
if(q + len[i] - 1 > r)
j = i;
}
}
void TestCase() {
scanf("%s", s);
int n = strlen(s);
int l = 0, r = n - 1;
while(l < r && s[l] == s[r])
++l, --r;
if(l >= r) {
puts(s);
return;
}
int n2 = r - l + 1;
manacher(s + l, len, n2);
int len1 = 0, len2 = 0;
for(int i = 0; i < (n2 << 1) - 1; ++i) {
int lmid = i / 2;
int l = (len[i] << 1) - (!(i & 1));
// printf("i=%d l=%d\n", i, l);
if(len[i] - 1 >= lmid) {
assert(len[i] - 1 == lmid);
int l = (len[i] << 1) - (!(i & 1));
len1 = max(len1, l);
}
}
reverse(len, len + (n2 << 1) - 1);
for(int i = 0; i < (n2 << 1) - 1; ++i) {
int lmid = i / 2;
int l = (len[i] << 1) - (!(i & 1));
// printf("i=%d l=%d\n", i, l);
if(len[i] - 1 >= lmid) {
assert(len[i] - 1 == lmid);
int l = (len[i] << 1) - (!(i & 1));
len2 = max(len2, l);
}
}
// printf("len1=%d len2=%d\n", len1, len2);
if(len1 >= len2) {
for(int i = 0; i < l; ++i)
putchar(s[i]);
for(int i = l, k = 0; k < len1; ++i, ++k)
putchar(s[i]);
for(int i = r + 1; i < n; ++i)
putchar(s[i]);
putchar('\n');
} else {
for(int i = 0; i < l; ++i)
putchar(s[i]);
for(int i = r, k = 0; k < len2; --i, ++k)
putchar(s[i]);
for(int i = r + 1; i < n; ++i)
putchar(s[i]);
putchar('\n');
}
return;
}
注意这个manacher模板里,长度为n的字符串下标为[0,n-1],len[i+j]表示区间[i,j]们的共同中心(i+j)/2向左右扩展出的最长的长度,易知这个是和奇偶性有关的。简单观察可以得到,len[i]实际代表的回文串长度为int l = (len[i] << 1) - (!(i & 1));
,那么求出每个区间的左中点(偶数区间是左中点,奇数区间可以合并进来),当len[i]>=1时说明左中点和右中点相同,把这个len值减去1,看看能否从lmid拓展到0。
*E - Bombs
一开始想着要在两次相邻的结果之间迭代,但是怎么想复杂度都是不对的,正确的思路是:显然答案是非递增的,很有可能可以通过这个性质控制复杂度。对于每个i位置,枚举每个x,然后验证x是否能成为答案,这个就意味着