D. Very Different Array
贪心

让绝对值最大,就是让值都尽可能分开,如果两个数组长度相同,结论是经典的,一个升序,一个降序,一一对应。但这里长度不一样,还是一个升序,一个降序,但是问题在于 A A A和 B B B的哪里对齐,极端情况就是开头和开头对齐,或结尾和结尾对齐,答案就是这两种取 m a x max max
Reverse XOR
位运算,思维

手玩可以发现如果 n n n的某一位是 1 1 1,其对称位置是 0 0 0,那么 s , r e v ( s ) s,rev(s) s,rev(s)在这两个位置就是 11 , 01 11,01 11,01或者 10 , 00 10,00 10,00,这与 s , r e v ( s ) s,rev(s) s,rev(s)矛盾,所以 n n n的二进制必须是回文的。
并且如果 n n n长度是奇数,那 r e v ( s ) , s rev(s),s rev(s),s在中心肯定是相等的, n n n在中心也就必须是 0 0 0
前后缀有 0 0 0无所谓,可以补 0 0 0使得两侧的 0 0 0个数相同,对结果无影响
所以总结就是,去掉忽略前后缀的 0 0 0,必须是二进制回文串,如果此时长度奇数,中心还必须是 0 0 0
Cobb
枚举+思维/sosdp

简单做法是,注意到 i ∗ j i*j i∗j的数量级别比 k ∗ ( a i o r a j ) k*(a_i or a_j) k∗(aioraj)大,所以答案肯定是在 i j ij ij都较大的区域取到的,可以只枚举 i , j i,j i,j较大的区间,暴力计算,大概只用枚举 [ n − k , n ] [n-k,n] [n−k,n]这一段,复杂度 O ( k 2 ) O(k^2) O(k2)
但这个只适用于 k k k较小,如果 k k k任意取,正解是 s o s d p sosdp sosdp。我们前面都在枚举 i , j i,j i,j,转换思路,不妨枚举 a i o r a j a_i or a_j aioraj,这样后一项就是确定的了,我们只需要确定,对于一个确定的 o r or or值, i ∗ j i*j i∗j的最大值,也就是维护,满足 a i , a j a_i,a_j ai,aj都是 o r or or值的子集的,最大和次大的 i , j i,j i,j
维护每个 m a s k mask mask的所有子集的最值,需要我们较快的遍历每个 m a s k mask mask的所有子集,这就是经典的 s o s d p sosdp sosdp。每个状态维护最大和次大的下标,转移时,两个状态的四个下标,取较大的两个。
初始化时,每个 m a s k mask mask赋值为这个值的最大的两个下标,不存在的话赋值 0 0 0。
最后计算答案,如果一个 m a s k mask mask至少有两个有效的下标,更新答案。
c
pii f[1 << 20];
void solve() {
int n, k;
cin >> n >> k;
map<int, set<int>>mp;
int mx = 0;
for (int i = 1; i <= n; i++) {
int x;
cin >> x;
mx = max(mx, __lg(x) + 1);
mp[x].insert(i);
if (mp[x].size() > 2) {
mp[x].erase(mp[x].begin());
}
}
for (int i = 0; i < (1 << mx); i++) {
if (mp.count(i)) {
if (mp[i].size() == 2) {
f[i] = {*mp[i].begin(), *mp[i].rbegin()};
} else {
f[i] = {*mp[i].begin(), 0};
}
} else {
f[i] = {0, 0};
}
}
for (int i = 0; i < mx; i++) {
for (int j = 0; j < (1 << mx); j++) {
if (j >> i & 1) {
vi t = {f[j].fi, f[j].se, f[j ^ (1 << i)].fi, f[j ^ (1 << i)].se};
sort(t.begin(), t.end());
f[j] = {t[3], t[2]};
}
}
}
int ans = -inf;
for (int i = 0; i < (1 << mx); i++) {
// cout<<i<<' '<<f[i].fi * f[i].se - k * i<<'\n';
if (f[i].fi && f[i].se) {
ans = max(ans, f[i].fi * f[i].se - k * i);
}
}
cout << ans << '\n';
}
E. Segment Sum
数位dp+状压+维护方案数和答案

出现的数位不超过 k k k种,用一个集合维护出现过的数位。要求的是合法数字的和,不是个数了,分别维护个数和数字的和,每一位数字的贡献可以根据,子问题方案数和这个数字的位置算出来。
c
int p[30];
int work(int x, int k) {
string s = to_string(x);
int n = s.size();
vvi f(n, vi(1 << 10, -1));//数位和
vvi g(n, vi(1 << 10, -1));//方案数
auto &&dfs = [&](auto &&dfs, int i, int mask, bool lim, bool lead)->pii{
if (i == n) {
return {0, (lead == 0)&&(__builtin_popcount(mask) <= k)};
}
if (!lim && !lead && f[i][mask] != -1) {
return {f[i][mask], g[i][mask]};
}
int up;
if (lim) {
up = s[i] - '0';
} else {
up = 9;
}
int cnt = 0, sum = 0;
for (int d = 0; d <= up; d++) {
pii res;
if (d == 0 && lead) {
res = dfs(dfs, i + 1, mask, 0, 1);
} else {
res = dfs(dfs, i + 1, mask | (1 << d), lim && (d == up), 0);
}
cnt = (cnt + res.se) % M2;
sum = (sum + res.se * d % M2 * p[n - i - 1] % M2 + res.fi) % M2;
}
if (!lim && !lead) {
f[i][mask] = sum;
g[i][mask] = cnt;
}
return {sum, cnt};
};
return dfs(dfs, 0, 0, 1, 1).fi;
}
void solve() {
int l, r, k;
cin >> l >> r >> k;
p[0] = 1;
for (int i = 1; i <= 20; i++) {
p[i] = p[i - 1] * 10 % M2;
}
cout << (work(r, k) - work(l - 1, k) + M2) % M2;
}
Subsequences Galore
sosdp+容斥原理

一个字符串数组 b b b,至少是其中一个字符串子序列的字符串个数,不太好求,但这是个并集大小,可以转化为容斥原理,计算出他所有元素的交集,然后乘上容斥系数累加即可。
但我们这里要对所有集合 m a s k mask mask都进行容斥,也就是枚举所有状态的所有子集,暴力枚举 O ( 4 n ) 或 O ( 3 n ) O(4^n)或O(3^n) O(4n)或O(3n)的,可以用 s o s d p sosdp sosdp降低到 O ( n 2 n ) O(n2^n) O(n2n)。这里就相当于用 s o s d p sosdp sosdp进行容斥原理,需要注意转移系数需要是容斥系数。
接下来唯一的问题就是怎么算元素交集的答案了:
对于一个字符串,他的所有子序列,由于字符串是升序的,就是每一段可以选任意个,答案是每一段的长度+1相乘,加一是因为这一段也可以不选,也是一种选择。
对于多个字符串,他们的公共子序列,需要每一种字符的出现次数都不超过任何一个字符串,所以把每种字符出现次数取最小值,然后类似前面的式子,加一,相乘。
C. Count Good Numbers
容斥

正难则反,计算至少包含一个一位数质因子的个数。一位数质因子只有 2 , 3 , 5 , 7 2,3,5,7 2,3,5,7,现在相当于求并集大小,计算交集大小容斥即可,交集大小就是区间内包含 2 , 3 , 5 , 7 , 2 ∗ 3 , 2 ∗ 5.. 2,3,5,7,2*3,2*5.. 2,3,5,7,2∗3,2∗5..等因子的数字个数,用 f l o o r ( x / f a c t o r ) floor(x/factor) floor(x/factor)即可得到
P5322 [BJOI2019] 排兵布阵
分组背包 排序 贪心

b b b数组的分配就是一个背包,每个位置有 [ 0 , m ] [0,m] [0,m]种选择,相当于一个分组背包,但这样的话复杂度就有 O ( n m 2 ) O(nm^2) O(nm2)了,需要优化
注意到得分只需要比 a [ i ] [ j ] ∗ 2 a[i][j]*2 a[i][j]∗2大,那么最节约的方式就是恰好 2 a [ i ] [ j 1 ] + 1 2a[i][j_1]+1 2a[i][j1]+1,更大的,直到下一个 2 a [ i ] [ j 2 ] + 1 2a[i][j_2]+1 2a[i][j2]+1之间的取值都是不优的,可以不用枚举,那么 b b b的每个位置,可能取值只有 O ( s ) O(s) O(s)个,总复杂度 O ( s n m ) O(snm) O(snm)
为了知道一个 b i b_i bi超过多少个 a ( i , j ) a(i,j) a(i,j),需要对 a a a的每一列排序。
E. Tufurama
离线+树状数组/在线+可持久化线段树

离线做法,本质是二位数点,满足 i < j , i < = a j , j < = a i i<j,i<=a_j,j<=a_i i<j,i<=aj,j<=ai,我们枚举 i i i,统计合法 j j j,首先为了满足 i < j i<j i<j,初始把所有 j j j插入树状数组,枚举到一个 j j j就把他删除;为了满足 i < = a j i<=a_j i<=aj,把 j j j按 a j a_j aj升序排序,双指针,把比 i i i小的 a j a_j aj删掉,注意一个 j j j不能被多次删除,维护一个标记记录每个 j j j是否被删除,此时剩下的都是合法 j j j,做 j < = a i j<=a_i j<=ai查询即可
在线做法,变换下条件: j < i , i < = a j , j < = a i j<i,i<=a_j,j<=a_i j<i,i<=aj,j<=ai,枚举 a i a_i ai,插入可持久化线段树,每次查询合法 j j j,首先 j j j下标要在 [ 1 , a i ] [1,a_i] [1,ai],由于 a i = 1 e 9 a_i=1e9 ai=1e9,准确说是 [ 1 , min ( n , a i ) ] [1,\min(n,a_i)] [1,min(n,ai)],对于在这个区间内的元素,查值满足 a j > = i a_j>=i aj>=i的个数,这是一个区间上的值域查询,可持久化线段树解决。