10.1
T1
给定一个长度为 n n n 的数组 a a a, Q Q Q 次询问每次询问给出 [ L , R ] [L,R] [L,R],求所有 [ L , R ] [L,R] [L,R] 的子区间 [ l , r ] [l,r] [l,r] 满足从 a l a_l al 到 a r a_r ar 中至少分别有一个奇数和一个偶数。
n , Q ≤ 5 × 1 0 5 n,Q\le 5\times 10^5 n,Q≤5×105。
这题的部分分高达 90 p t s 90\mathrm{pts} 90pts,一个 O ( n Q ) O(nQ) O(nQ) 的暴力和一个至多一个奇数或一个偶数的特殊性质。暴力就考虑对于 i ∈ [ L , R ] i\in [L,R] i∈[L,R],以 i i i 为左端点的区间有几个,可以发现若 i i i 为奇数那么左端点必须在一个偶数之前, i i i 为偶数同理。暴力就做完了。特殊性质分显然左右端点必须在那个特殊数字的左右两边。
namespace Subtask0 {
const int maxn = 2005;
int pre0[maxn], pre1[maxn];
int work() {
for (int qq = 1; qq <= q; qq ++) {
int l = Q[qq].L, r = Q[qq].R;
int ans = 0; pre0[l] = (a[l] == 0 ? l : -1), pre1[l] = (a[l] == 1 ? l : -1);
for (int i = l + 1; i <= r; i ++) {
pre0[i] = (a[i] == 0 ? i : pre0[i - 1]), pre1[i] = (a[i] == 1 ? i : pre1[i - 1]);
if (a[i] == 0 && pre1[i] != -1) ans += pre1[i] - l + 1;
if (a[i] == 1 && pre0[i] != -1) ans += pre0[i] - l + 1;
} printf("%d\n", ans);
} return 0;
}
}
namespace Subtask1 {
const int maxn = 5e5 + 5;
int pos;
int work(int x) {
for (int i = 1; i <= n; i ++)
if ((x == 1 && a[i] == 1) || (x == n - 1 && a[i] == 0))
{ pos = i; break; }
for (int i = 1; i <= q; i ++) {
if (Q[i].L <= pos && pos <= Q[i].R)
printf("%lld\n", 1ll * (pos - Q[i].L + 1) * (Q[i].R - pos + 1) - 1);
else puts("0");
} return 0;
}
}
正解考虑莫队,按照第一档暴力分的方法拆贡献即可。但是要分类讨论 i i i 作为左端点还是右端点产生的更多的区间。时间复杂度 O ( n n ) O(n\sqrt{n}) O(nn)。
namespace STD {
const int maxn = 5e5 + 5;
int block_id[maxn];
void init() {
int Length = sqrt(n);
for (int i = 1, k = 1; i <= n; i += Length, k ++)
for (int j = i; j <= min(n, i + Length); j ++)
block_id[j] = k;
}
struct _ {
int L, R, id;
_(Query x0 = {0, 0}, int i0 = 0) {
L = x0.L, R = x0.R, id = i0;
}
bool operator<(const _ &oth) const {
return block_id[L] == block_id[oth.L] ? R < oth.R : block_id[L] < block_id[oth.L];
}
} p[maxn];
int pre[maxn][2], suf[maxn][2];
int nowl = 1, nowr = 0; ll ans = 0;
void add(int i, int tp) {
if (tp == 0) {
if (suf[i][a[i] ^ 1] != -1 && suf[i][a[i] ^ 1] <= nowr)
ans += nowr - suf[i][a[i] ^ 1] + 1;
} else {
if (pre[i][a[i] ^ 1] != -1 && pre[i][a[i] ^ 1] >= nowl)
ans += pre[i][a[i] ^ 1] - nowl + 1;
}
}
void del(int i, int tp) {
if (tp == 0) {
if (suf[i][a[i] ^ 1] != -1 && suf[i][a[i] ^ 1] <= nowr)
ans -= nowr - suf[i][a[i] ^ 1] + 1;
} else {
if (pre[i][a[i] ^ 1] != -1 && pre[i][a[i] ^ 1] >= nowl)
ans -= pre[i][a[i] ^ 1] - nowl + 1;
}
}
ll res[maxn];
int main() {
pre[1][a[1]] = 1, pre[1][a[1] ^ 1] = -1;
for (int i = 2; i <= n; i ++)
pre[i][a[i]] = i, pre[i][a[i] ^ 1] = pre[i - 1][a[i] ^ 1];
suf[n][a[n]] = 1, suf[n][a[n] ^ 1] = -1;
for (int i = n - 1; i; i --)
suf[i][a[i]] = i, suf[i][a[i] ^ 1] = suf[i + 1][a[i] ^ 1];
for (int i = 1; i <= q; i ++) p[i] = _(Q[i], i);
init(), sort(p + 1, p + q + 1);
for (int i = 1; i <= q; i ++) {
int l = p[i].L, r = p[i].R;
for (; l < nowl; nowl --, add(nowl, 0));
for (; nowr < r; nowr ++, add(nowr, 1));
for (; l > nowl; del(nowl, 0), nowl ++);
for (; nowr > r; del(nowr, 1), nowr --);
res[p[i].id] = ans;
}
for (int i = 1; i <= q; i ++)
printf("%lld\n", res[i]);
return 0;
}
}
T2
给一个 n × m n\times m n×m 的 01 矩阵 A A A,你可以将 A A A 中 0 0 0 行或若干行的元素 0 0 0 变 1 1 1、 1 1 1 变 0 0 0,求操作能够得到的最大的 全 0 0 0 矩阵。(原题中全 1 1 1 也合法,但是把 A A A 所有行都反转一下就变成全 0 0 0 矩阵了)
n , m ≤ 3000 n,m\le 3000 n,m≤3000。
赛时先考虑了直接枚举正方形边长 k k k,对于正方形的左上角 ( x 0 , y 0 ) (x_0,y_0) (x0,y0),合法当且仅当每一行中的元素都是全 0 0 0 或全 1 1 1。那我们做一个类似于滑动窗口的东西,先枚举列再枚举行。然后发现 k k k 显然有单调性,于是把枚举换成二分就能过了。时间复杂度 O ( n 2 log n ) O(n^2\log n) O(n2logn)。
#include <bits/stdc++.h>
using namespace std;
namespace STD {
const int maxn = 3005;
int n, m, f[maxn][maxn]; char pic[maxn][maxn];
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++) {
scanf("%s", pic[i] + 1);
for (int j = 1; j <= m; j ++)
f[i][j] = (pic[i][j] == 'W') + f[i][j - 1];
} int ans = 1;
for (int l = 1, r = min(n, m); l <= r; ) {
int k = (l + r) >> 1; bool ok = 0;
for (int j = k, tot = 0; j <= m; j ++) {
tot = 0;
for (int i = 1; i < k; i ++)
if (f[i][j] - f[i][j - k] != k && f[i][j] - f[i][j - k] != 0)
tot ++;
for (int i = k; i <= n; i ++) {
if (f[i][j] - f[i][j - k] != k && f[i][j] - f[i][j - k] != 0)
tot ++;
if (tot == 0) { ok = 1; break; }
if (f[i - k + 1][j] - f[i - k + 1][j - k] != k && f[i - k + 1][j] - f[i - k + 1][j - k] != 0)
tot --;
} if (ok) break;
} if (ok) ans = k, l = k + 1;
else r = k - 1;
} return printf("%d\n", ans * ans), 0;
}
}
int main() {
freopen("art.in", "r", stdin), freopen("art.out", "w", stdout);
return STD :: main();
}
T3
有一个长度为 n n n 的数组 H H H, Q Q Q 次操作分为两种:
- 给定 i , h i,h i,h,令 H i ← h H_i\gets h Hi←h,保证 H i < h H_i< h Hi<h;
- 给定字符 c c c 和整数 i i i,执行下面的过程直到找不到要求的值:
- 若 c = L c=\texttt{L} c=L,那么在 [ 1 , i − 1 ] [1,i-1] [1,i−1] 中找到最大的 j j j 使得 H j > H i H_j> H_i Hj>Hi,然后令 i ← j , c ← R i\gets j,c\gets \texttt{R} i←j,c←R;
- 若 c = R c=\texttt{R} c=R,那么在 [ i + 1 , n ] [i+1,n] [i+1,n] 中找到最小的 j j j 使得 H j > H i H_j>H_i Hj>Hi,然后令 i ← j , c ← L i\gets j,c\gets \texttt{L} i←j,c←L。
- 最后输出 i i i 的值。
n , Q ≤ 2 × 1 0 5 n,Q\le 2\times 10^5 n,Q≤2×105, H i H_i Hi 任何时刻互不相同,保证数据完全随机。
有一档 n , Q ≤ 3000 n,Q\le 3000 n,Q≤3000 和一档保证无修改操作有 60 p t s 60\mathrm{pts} 60pts。前者维护出从 i i i 出发往两个方向跳到的点,用单调栈轻松维护,每次修改直接暴力再算一遍,询问时暴力跳即可,每轮每个点显然最多跳到 1 1 1 次于是时间复杂度 O ( n Q ) O(nQ) O(nQ)。后者直接把每个点的终点算出来 O ( 1 ) O(1) O(1) 回答。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e5 + 5;
int n, m, h[N];
struct Record {
int tp;
int id, hgt;
int way, st;
} r[N];
namespace Subtask0 {
const int maxn = 5005;
int w[maxn][2];
int st[maxn], top;
void getway() {
top = 0;
for (int i = 1; i <= n; i ++) {
w[i][0] = w[i][1] = 0;
for (; top && h[st[top]] < h[i]; w[st[top --]][1] = i);
w[i][0] = (top ? st[top] : 0), st[++ top] = i;
}
}
int main() {
getway();
for (int i = 1; i <= m; i ++) {
if (r[i].tp == 0)
h[r[i].id] = r[i].hgt, getway();
else {
int now = r[i].st;
for (int way = r[i].way; w[now][way] != 0; now = w[now][way], way ^= 1);
printf("%d\n", now);
}
} return 0;
}
}
namespace Subtask1 {
const int maxn = 2e5 + 5;
int w[maxn][2], ed[maxn][2];
int st[maxn], top;
void getway() {
top = 0;
for (int i = 1; i <= n; i ++) {
w[i][0] = w[i][1] = 0, ed[i][0] = ed[i][1] = -1;
for (; top && h[st[top]] < h[i]; w[st[top --]][1] = i);
w[i][0] = (top ? st[top] : 0), st[++ top] = i;
}
for (int i = 1; i <= n; i ++)
for (int j = 0; j < 2; j ++) {
int now = i, way = j;
for (; w[now][way] != 0 && ed[now][way] == -1; now = w[now][way], way ^= 1);
if (w[now][way] == 0) ed[i][j] = now;
else ed[i][j] = ed[now][way];
}
}
int main() {
getway();
for (int i = 1; i <= m; i ++)
printf("%d\n", ed[r[i].st][r[i].way]);
return 0;
}
}
以为完全随机的数据是留给一些基于随机化的算法的,我不会,于是就没往下继续拼分。但是数据随机,意味着每次跳一步期望令自己的排名,也就是相对大小翻倍(减半),故期望跳 O ( log n ) O(\log n) O(logn) 次就能找到终点。这也是我特殊性质档暴力找终点复杂度也正确的原因。若数据不随机那一档加个栈维护经过点,每次把栈里的点一起计算,复杂度就变成 O ( n ) O(n) O(n) 了。那我们用线段树维护一个最大值和位置,每次按照题意模拟然后在线段树上二分就做完了。时间复杂度 O ( Q log 2 n ) O(Q\log^2n) O(Qlog2n)。也有不依赖于数据随机的笛卡尔树做法,但是没有听懂。
namespace STD {
const int maxn = 2e5 + 5;
namespace SegmentTree {
int mx[maxn << 2];
void update(int rt) { mx[rt] = max(mx[rt << 1], mx[rt << 1 | 1]); }
#define lson l, mid, rt << 1
#define rson mid + 1, r, rt << 1 | 1
void build(int l, int r, int rt) {
if (l == r) return mx[rt] = h[l], void(0);
int mid = (l + r) >> 1;
build(lson), build(rson), update(rt);
}
int queryL(int l, int r, int rt, int pos, int val) {
if (pos < l || mx[rt] <= val) return -1;
if (l == r) return l;
int mid = (l + r) >> 1, resr = queryL(rson, pos, val);
return resr == -1 ? queryL(lson, pos, val) : resr;
}
int queryR(int l, int r, int rt, int pos, int val) {
if (r < pos || mx[rt] <= val) return -1;
if (l == r) return l;
int mid = (l + r) >> 1, resl = queryR(lson, pos, val);
return resl == -1 ? queryR(rson, pos, val) : resl;
}
void modify(int l, int r, int rt, int k, int now) {
if (l == r) return mx[rt] = k, void(0);
int mid = (l + r) >> 1;
if (now <= mid) modify(lson, k, now);
else modify(rson, k, now);
update(rt);
}
} using namespace SegmentTree;
int main() {
build(1, n, 1);
for (int i = 1; i <= m; i ++) {
if (r[i].tp == 0)
h[r[i].id] = r[i].hgt, modify(1, n, 1, r[i].hgt, r[i].id);
else {
int now = r[i].st, way = r[i].way;
for (int nxt; ; now = nxt, way ^= 1) {
if (way == 0) nxt = queryL(1, n, 1, now - 1, h[now]);
else nxt = queryR(1, n, 1, now + 1, h[now]);
// cout << now << '\n';
if (nxt == -1) break;
}
printf("%d\n", now);
}
} return 0;
}
}
随机数据下跑的还挺快的。
T4
拿了一个
“ 不 可 以 , 总 司 令 。 \Huge{\color{red}不}{\color{orange}可}{\color{blue}以}{\color{brown},}{\color{black}总}{\color{green}司}{\color{yellow}令}{\color{gray}。} 不可以,总司令。”
的 10 p t s 10\mathrm{pts} 10pts。赛时想到了要使用最短路,但是发现不会建边。需要知道一个串本质不同的回文子串数目是 O ( n ) O(n) O(n) 的,于是我们可以考虑把这些回文子串看成点然后建边。这样边数是 O ( n ) O(n) O(n) 的,在跳激活串的时候只需相邻两串建边即可。对于增删字符的部分,我们考虑在 manacher 中 m a x r maxr maxr 更新的时候进行建边。然后就做完了。猜想若学过 PAM 就能一眼看出在 PAM 上跑最短路,可是我不会。字符串长度之和是 1 0 6 10^6 106 的,于是这么建边本质不同回文子串即点数是正确的。
#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define ull unsigned ll
template<typename T>
void read(T &x) {
x = 0; char c = getchar(); bool f = 0;
for (; c < '0' || c > '9'; c = getchar())
if (c == '-') f = 1;
for (char c = getchar(); c >= '0' && c <= '9'; c = getchar())
x = (x << 3) + (x << 1) + (c ^ 48);
if (f) x = -x;
}
const int maxn = 2000005;
const ull base = 113;
const int inf = 0x3f3f3f3f;
int m, K, S, T;
char s[maxn];
int c[26], d[26];
ull B[maxn], h[maxn];
int len[maxn << 1];
map<ull, int> lst, p[maxn];
map<ull, bool> v;
int tot, cnt, head[maxn];
struct Edge { int to, nxt, w; } e[maxn << 2];
struct Node {
int x, d;
bool operator < (const Node &a) const {
return d > a.d;
}
};
int dis[maxn];
bool vis[maxn];
void addEdge(int u, int v, int w) {
e[++ cnt] = { v, head[u], w }, head[u] = cnt;
}
void init() {
B[0] = 1;
for (int i = 1; i <= 100000; i ++)
B[i] = B[i - 1] * base;
}
ull val(int l, int r) {
return h[r] - h[l - 1] * B[r - l + 1];
}
void check(int x, ull tmp) {
if (v.find(tmp) == v.end()) {
v[tmp] = 1;
p[x][tmp] = ++tot;
if (lst.find(tmp) != lst.end()) {
int y = lst[tmp], w = abs(x - y);
if (w <= K) addEdge(p[y][tmp], tot, w), addEdge(tot, p[y][tmp], w);
} lst[tmp] = x;
}
}
char a[maxn << 1];
void manacher(char s[], int x) {
int L = strlen(s + 1), m = 1;
a[1] = '*';
for (int i = 1; i <= L; i ++)
a[++ m] = s[i], a[++ m] = '*';
int r = 0, mid = 0; v.clear();
for (int i = 1; i <= m; i ++) {
len[i] = 1;
if (i <= r) len[i] = min(len[mid + mid - i], r - i + 1);
int pos = len[i];
while (i - len[i] && i + len[i] <= m && a[i - len[i]] == a[i + len[i]])
len[i] ++;
if (i + len[i] - 1 > r) r = i + len[i] - 1, mid = i;
for (int j = pos; j <= len[i]; j ++) {
if (a[i - j + 1] == '*') continue;
int l = (i - j + 1) >> 1, r = (i + j - 1) >> 1;
ull A = val(l, r); check(x, A);
if (l + 1 <= r - 1) {
ull B = val(l + 1, r - 1);
addEdge(p[x][B], p[x][A], c[s[l] - 'a']);
addEdge(p[x][A], p[x][B], d[s[l] - 'a']);
}
}
}
}
priority_queue<Node> q;
void Dijkstra(int S) {
for (int i = 1; i <= tot; i ++)
dis[i] = inf, vis[i] = 0;
dis[S] = 0;
q.push((Node){S, 0});
while (!q.empty()) {
int x = q.top().x;
q.pop();
if (vis[x]) continue;
vis[x] = 1;
for (int i = head[x]; i; i = e[i].nxt) {
int to = e[i].to;
if (dis[to] > dis[x] + e[i].w)
dis[to] = dis[x] + e[i].w, q.push((Node){to, dis[to]});
}
}
}
int main() {
freopen("eng.in", "r", stdin), freopen("eng.out", "w", stdout);
init(), scanf("%d %d", &m, &K);
int L;
for (int i = 1; i <= m; i ++) {
scanf("%s", s + 1);
L = strlen(s + 1);
for (int j = 1; j <= L; j++)
h[j] = h[j - 1] * base + s[j];
for (int j = 0; j < 26; j++)
scanf("%d", &c[j]);
for (int j = 0; j < 26; j++)
scanf("%d", &d[j]);
manacher(s, i);
}
int x; scanf("%d%s", &x, s + 1);
L = strlen(s + 1);
ull A = 0;
for (int i = 1; i <= L; i ++)
A = A * base + s[i];
S = p[x][A];
scanf("%d%s", &x, s + 1);
L = strlen(s + 1);
A = 0;
for (int i = 1; i <= L; i ++)
A = A * base + s[i];
T = p[x][A];
if (S == 0 || T == 0) return puts("-1"), 0;
Dijkstra(S);
if (dis[T] == inf) puts("-1");
else printf("%d\n", dis[T]);
return 0;
}