长春集训 Day 5 - 模拟赛
长春集训 Day 5 - 模拟赛
爆零了。思考了一下,T1 由于昨天学习双指针,思维模式发生了转变,而且对于贪心策略过程中“不劣”的理解不够到位;T2 思维没有问题,stl 复杂度不会估计(指不知道 substr 是 \(O(n)\) 的);T3 写代码时逻辑不够精准。心态问题不太大。
感谢小升老师请的汉堡 /bx
需要注意,数据结构一定起到的是辅助作用,不要先往数据结构的角度考虑问题。除非这题纯考数据结构(如 T3)。
T1 - 嗡嗡蜜蜂
Description
给定 \(n\) 个线段,其中第 \(i\) 个线段为区间 \([l_i,r_i]\)。
对于数对 \((i,j)\),它是可计算的,当且仅当线段 \(i\) 与线段 \(j\) 有交。如果它是可计算的,那么它的权值是被两线段覆盖的部分中被且仅被其中一个线段覆盖的区间长度之和。
对于所有可计算的数对,计算它们权值的最大值。如果不存在可计算的数对,输出 \(0\)。
\(1 \le n \le 2 \times 10^5,\ 1 \le l_i \le r_i \le 10^8\)。
Reflection
场上想的双指针是完全错误的,因为双指针显然取不到所有区间。
我们需要注意,双指针适合维护 答案具有一定单调性、增删元素后容易判断答案合法性 的问题。
这道题的答案显然不具有单调性(容易构造出反例)。所以它取不到所有区间。
一会儿做几道双指针的题感受一下。
Solution
“数对的权值”的定义似乎很弱,首先我们想一下“数对的权值”到底是什么,怎么转化。
通过简单分析可知,当两个线段形如下面这样排列时:
s
--------------
t
======================
那么答案显然是 \((t_r-s_r)+(t_l-s_l)\ \ (1)\)。
另外,当两个线段形如下面这样排列时:
s
---------
t
================================
那么答案显然是 \((t_r-s_r) + (s_l-t_l)\ \ (2)\)。
我们容易想到把 \((1)\) 式和 \((2)\) 式化成只跟一个线段有关的东西,于是
可以把括号里的东西看作一个整体,而“数对的权值”就是这两个整体的差值(但需要看属于 \((1)\) 情况还是 \((2)\) 情况)。
先考虑朴素做法。为防止贡献算重,发现将所有线段按左端点排序,然后按顺序遍历所有线段,计算当前遍历到的线段与它之前线段的贡献,一定可行。
假设当前遍历到的线段为 \(t\),那么对于一个线段 \(s(s_l \le t_l)\),它与 \(t\) 有交当且仅当 \(s_r \ge t_l\)。于是可以分类讨论:
- 当 \(s_r \in [t_l, t_r]\) 时,属于情况 \((1)\),统计区间 \([t_l,t_r]\) 内 \(s_l+s_r\) 的最小值即可。
- 当 \(s_r \in (t_r,+\infin)\) 时,属于情况 \((2)\),统计区间 \((t_r,+\infin)\) 内 \(s_l-s_r\) 的最小值即可。
于是对于每个线段 \(s\),在 \(s_r\) 的位置记录权值 \(s_l+s_r\) 与 \(s_l-s_r\) 的最小值即可。想到线段树。
为方便统计,先统计答案再加入线段。
由于值域 \(10^8\),需要动开线段树(或离散化)。
注意细节:
- 动开线段树至少需要 \(N \log V\) 的空间。
- 当存在多个线段的右端点相同时,涉及权值覆盖问题(必须覆盖成最小值),需要特判一下。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 2e5+5, V = 1e8+5;
int n, root[2], ans;
struct Segment {
int l, r;
friend bool operator < (Segment x, Segment y) {
if (x.l == y.l) return x.r < y.r;
return x.l < y.l;
}
} ele[N];
// 单点改,区间查询 min
struct DynamicSegmentTree {
#define mid ((l + r) / 2)
#define lson t[id].lc
#define rson t[id].rc
int tot = 0;
struct Node {
int lc, rc, minn;
Node() {
lc = rc = 0;
minn = inf;
}
} t[N << 4];
void pushup(int id) {
t[id].minn = min(lson ? t[lson].minn : inf, rson ? t[rson].minn : inf);
}
void modify(int &id, int l, int r, int pos, int val) {
if (id == 0) id = ++tot;
if (l == r) {
t[id].minn = min(t[id].minn, val);
return;
} else {
if (pos <= mid) modify(lson, l, mid, pos, val);
else modify(rson, mid + 1, r, pos, val);
pushup(id);
}
}
int query(int id, int l, int r, int ql, int qr) {
if (id == 0) return inf;
if (ql == l and qr == r) {
return t[id].minn;
} else {
if (qr <= mid) return query(lson, l, mid, ql, qr);
else if (ql > mid) return query(rson, mid + 1, r, ql, qr);
else return min(query(lson, l, mid, ql, mid), query(rson, mid + 1, r, mid + 1, qr));
}
}
#undef mid
#undef lson
#undef rson
} seg[2];
signed main() {
cin.tie(0) -> sync_with_stdio(0);
// file(bee);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> ele[i].l >> ele[i].r;
}
sort(ele+1, ele+1+n);
for (int i = 1; i <= n; i++) {
// JUDGE
int minval0 = seg[0].query(root[0], 1, V, ele[i].l, ele[i].r);
ans = max(ans, ele[i].l + ele[i].r - minval0);
int minval1 = seg[1].query(root[1], 1, V, ele[i].r, V);
ans = max(ans, ele[i].l - ele[i].r - minval1);
// UPDATE
seg[0].modify(root[0], 1, V, ele[i].r, ele[i].l + ele[i].r);
seg[1].modify(root[1], 1, V, ele[i].r, ele[i].l - ele[i].r);
}
cout << ans;
return 0;
}
T2 - 错乱的集合
Description
给定 \(n\) 个字符串,其中第 \(i\) 个字符串为 \(s_i\),其字符集为所有小写英文字母。对所有字符串,取它们的所有非空前缀(包含本身),并将它们按照一定顺序扔到一个集合里。
定义字符串 \(s\) 和 \(t\) 广义相同当且仅当 \(s=t\) 或 \(|s|>1 \land s_{2 \cdots |s|} = t\) 或 \(|t|>1 \land s = t_{2\cdots |t|}\)。
该集合中不能存在任何广义相同的字符串。求按照某种顺序将它们放入集合后,集合中元素个数的最大值。
\(1 \le n \le 10^6,\ \sum |s_i| \le 10^6\)。
Reflection
场上想的 40 分暴力是对的,但是使用了大量 substr 这种复杂度极高的 stl 方法,因此 TLE 了。
事实上,给定一些物品之间的一些两两互不依存关系,可以转化为最大权独立集问题。
由于这题的依存关系一定形如一棵树,所以可以转化为“没有上司的舞会”,简单 DP 即可。
Solution
新学习了字符串哈希。
字符串哈希是这样一种算法,它使得每个字符串都能表示为一个掩码(称为哈希值),且掩码具有可解释性。
由于字符集是小写英文字母,所以我们将一个字符串视作一个 \(27\) 进制数对 \(M\) 取模的结果(进制开大一点,为的是将空格算入字符集,避免被卡),其中 \(M\) 是一个极大的质数。此题中,我们可取:
const int M = 11451419198101145;
事实上,当 \(M\) 不为质数(但必须很大)时,哈希算法也有概率有可行性。具体可以自己调试。
一个空串的哈希值为 \(0\)。我们遍历一个字符串 \(s\) 的所有前缀,其实就是往空串里添加若干字符,并依次获取它们的哈希值。实现如下:
inline int add_char(int hash, char c) {
return (hash * 27 + (c - 'a' + 1)) % mod;
}
由题意可知,对于任意字符串 \(s\),其 \([2,|s|]\) 的子串与其不能共存。因此,我们想到,如果已知 \(s\) 的哈希值,如何搞出来它 \([2,|s|]\) 子串的哈希值。
这其实并不难。注意到这样的子串仅仅是原 \(s\) 被砍掉一个第一位所生成的结果,所以对于一个 \(s\),我们记录它的第一位是什么,并记录它的长度,则显然可以按如下方式计算子串的哈希值:
inline int get_suf(Element tmp) {
return (tmp.hash - tmp.head * pow27[tmp.len] % mod + mod) % mod;
}
其中 pow27[i] 表示 \(27^i\) 对 \(M\) 取模的结果。
这样的话,我们使用字符串哈希,可以在 \(O(1)\) 复杂度内取到一个字符串 \(s\) 的 \([2,|s|]\) 子串,无需 substr 的 \(O(n)\) 复杂度。根据广义相同字符串的定义,显然要将所有字符串哈希扔到 set<int> 中去重。这虽然只是一个哈希值,但如果我们建立 map<int, int> 映射表,就可以将 set<int> 中的一个哈希值映射到一个按顺序分配的索引。
显然字符串之间的两两不共存关系一定不能出现环,所以这些关系应该构成一个森林。因此,我们将 map<int, int> 分配的索引作为一个字符串唯一的点编号,建图在森林里跑树上最大权独立集即可。多测要清空。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x ".in", "r", stdin);
#define fileout(x) freopen(#x ".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e6 + 5, mod = 11451419198101145;
int T, n;
struct Element {
int len, head, hash;
friend bool operator < (Element x, Element y) {
if (x.len == y.len) return x.hash < y.hash;
return x.len < y.len;
}
friend bool operator == (Element x, Element y) {
return x.hash == y.hash;
}
};
set<Element> ele;
int tot;
map<int, int> mp;
vector<int> g[N];
int dp[N][2], vis[N];
void add_edge(int u, int v) {
g[u].push_back(v);
g[v].push_back(u);
}
int pow27[N];
inline int add_char(int hash, char c) {
return (hash * 27 + (c - 'a' + 1)) % mod;
}
inline int get_suf(Element tmp) {
return (tmp.hash - tmp.head * pow27[tmp.len] % mod + mod) % mod;
}
void DFS(int u, int fa) {
vis[u] = 1;
dp[u][1] = 1;
for (auto v : g[u]) {
if (v == fa)
continue;
DFS(v, u);
dp[u][0] += max(dp[v][0], dp[v][1]);
dp[u][1] += dp[v][0];
}
}
void solve() {
ele.clear(); mp.clear();
memset(g, 0, sizeof g);
memset(dp, 0, sizeof dp);
memset(vis, 0, sizeof vis);
cin >> n;
tot = 0;
for (int i = 1; i <= n; i++) {
string s; cin >> s;
int hash = 0;
for (int j = 0; j < s.length(); j++) {
hash = add_char(hash, s[j]);
ele.insert({j, s[0]-'a'+1, hash});
}
}
for (auto tmp : ele) {
int len = tmp.len, head = tmp.head, hash = tmp.hash;
mp[hash] = ++tot;
int suf = get_suf(tmp);
if (mp.count(suf)) add_edge(mp[suf], tot);
}
int ans = 0;
for (int i = 1; i <= tot; i++) {
if (not vis[i]) {
DFS(i, -1);
ans += max(dp[i][0], dp[i][1]);
}
}
cout << ans << '\n';
}
signed main() {
cin.tie(0)->sync_with_stdio(0);
// file(set);
pow27[0] = 1;
for (int i = 1; i <= 1e6; i++) {
pow27[i] = pow27[i-1] * 27 % mod;
}
cin >> T;
while (T--) {
solve();
}
return 0;
}
T3 - 01 串
Description
给你一个长度为 \(n\) 的 \(01\) 串,有 \(q\) 次询问,每次询问可能为下面三种操作之一:
- 选择区间 \([l,r]\) 并将该子段降序排列;
- 选择区间 \([l,r]\) 并将该子段升序排列;
- 选择区间 \([l,r]\) 并询问该区间对应的 \(01\) 串在十进制下对 \(10^9+7\) 取模的结果。
\(1 \le n,q \le 2 \times 10^5\)。
Reflection
场上把这个东西一眼秒了,但是写代码过程中产生了很难发现的错误,然后死活改不出来。
所以写代码的时候一定要细心,逻辑清晰。过早的优化是万恶之源。
Solution
发现排序操作一定把区间弄成 00...011...1 或 11...100...0 这种形式的,等价于统计区间内 \(1\) 或 \(0\) 的个数,并将它们搁置到区间的最靠左侧或右侧,另一边用相反的数字填充。
所以我们需要一个支持区间修改、区间查询 \(1\) 的个数的数据结构,想到最常用的线段树。合并和懒标记下传是十分简单的。
至于区间的十进制权值,考虑如何 pushup。显然一个区间的权值,等于它左子区间的权值和右子区间的权值拼到一起的结果,可以直接将左子区间乘上 \(2\) 的右子区间长度次方实现左移效果。
取模运算同样是简单的,使用快速幂即可。注意做完减法后可能出现负数,需要将模数加回来。
Code
需要特判长度为 \(0\) 的区间无法 query,否则会 RE。
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 2e5+5, mod = 1e9+7;
int pow2[N];
string s; int n, q;
int qpow(int x, int y) {
if (y == 0) return 1;
int tmp = qpow(x, y/2);
if (y & 1) return tmp * tmp % mod * x % mod;
return tmp * tmp % mod;
}
struct Seg {
#define mid ((l + r) / 2)
#define lson (id << 1)
#define rson (id << 1 | 1)
struct Node {
int siz;
int cnt1;
int val;
int lzy;
} t[N << 2];
void pushup(int id) {
t[id].cnt1 = t[lson].cnt1 + t[rson].cnt1;
t[id].val = (1ll * t[lson].val * pow2[t[rson].siz] + t[rson].val) % mod;
}
void setlzy(int id, int lzy) {
if (lzy == 1) {
t[id].lzy = 1;
t[id].cnt1 = t[id].siz;
t[id].val = pow2[t[id].siz] - 1;
if (t[id].val < 0) t[id].val += mod;
} else if (lzy == 2) {
t[id].lzy = 2;
t[id].cnt1 = 0;
t[id].val = 0;
}
}
void pushdown(int id) {
if (t[id].lzy != 0) {
setlzy(lson, t[id].lzy);
setlzy(rson, t[id].lzy);
t[id].lzy = 0;
}
}
void build(int id, int l, int r) {
t[id].siz = r - l + 1;
if (l == r) {
t[id].cnt1 = (s[l] == '1');
t[id].val = (s[l] == '1');
} else {
build(lson, l, mid);
build(rson, mid + 1, r);
pushup(id);
}
}
void modify(int id, int l, int r, int ql, int qr, int lzy) {
if (ql == l and qr == r) {
setlzy(id, lzy);
return;
} else {
pushdown(id);
if (qr <= mid) modify(lson, l, mid, ql, qr, lzy);
else if (ql > mid) modify(rson, mid + 1, r, ql, qr, lzy);
else {
modify(lson, l, mid, ql, mid, lzy);
modify(rson, mid + 1, r, mid + 1, qr, lzy);
}
pushup(id);
}
}
int query_cnt1(int id, int l, int r, int ql, int qr) {
if (ql == l and qr == r) {
return t[id].cnt1;
} else {
pushdown(id);
if (qr <= mid) return query_cnt1(lson, l, mid, ql, qr);
else if (ql > mid) return query_cnt1(rson, mid + 1, r, ql, qr);
else return query_cnt1(lson, l, mid, ql, mid) + query_cnt1(rson, mid + 1, r, mid + 1, qr);
}
}
int query_val(int id, int l, int r, int ql, int qr) {
if (ql == l and qr == r) {
return t[id].val;
} else {
pushdown(id);
if (qr <= mid) return query_val(lson, l, mid, ql, qr);
else if (ql > mid) return query_val(rson, mid + 1, r, ql, qr);
else return (query_val(lson, l, mid, ql, mid) * pow2[qr-mid] + query_val(rson, mid + 1, r, mid + 1, qr)) % mod;
}
}
#undef mid
#undef lson
#undef rson
} seg;
signed main() {
cin.tie(0) -> sync_with_stdio(0);
file(01);
cin >> s;
cin >> q;
n = s.length(); s = ' ' + s;
pow2[0] = 1;
for (int i = 1; i <= n; i++) {
pow2[i] = 2ll * pow2[i-1] % mod;
}
seg.build(1, 1, n);
while (q--) {
int op, L, R; cin >> op >> L >> R; int l = min(L,R), r = max(L,R);
if (op == 1) {
int cnt1 = seg.query_cnt1(1, 1, n, l, r);
if (cnt1-1 >= 0) seg.modify(1, 1, n, l, l+cnt1-1, 1);
if (r-l-cnt1 >= 0) seg.modify(1, 1, n, l+cnt1, r, 2);
} else if (op == 2) {
int cnt0 = r - l + 1 - seg.query_cnt1(1, 1, n, l, r);
if (cnt0-1 >= 0) seg.modify(1, 1, n, l, l+cnt0-1, 2);
if (r-l-cnt0 >= 0) seg.modify(1, 1, n, l+cnt0, r, 1);
} else {
cout << seg.query_val(1, 1, n, l, r) % mod << '\n';
}
}
return 0;
}
T4 - 尾声
Final days, on the planet
Here we are, on the ground
——Michael Kiwanuka《Final Days》
Description
定义一段长度为 \(k\) 的序列 \(b\) 的权值为 \(\sum\limits_{i=1}^k \sum\limits_{j=i}^k b_j\)。
给定长度为 \(n\) 的序列 \(a\),对于每个 \(k \in [1,n]\),求:对于 \(a\) 的所有长度为 \(k\) 的子序列(无需连续)而言,这些子序列权值的最大值。
Solution
\(O(n^2)\) 做法有 24 pts。
注意到
因此对于每个数字,它的数值前面的系数为当前选定子序列的长度。可以设 \(dp_{i,j}\) 表示考虑前 \(i\) 个数,选入 \(j\) 个数加入子序列的最大权值。显然有
每个数只能选一次,类似 01 背包状物,可以滚掉一维。
可以用 Splay 优化到 \(O(n \log n)\),但我还不会。
Code
#include <bits/stdc++.h>
// #pragma GCC optimize(2)
#define int long long
#define inf 1e18
#define debug cout << '!';
#define filein(x) freopen(#x".in", "r", stdin);
#define fileout(x) freopen(#x".out", "w", stdout);
#define file(x) filein(x) fileout(x)
using namespace std;
const int N = 1e5+5;
int n, a[N], dp[N];
signed main() {
cin.tie(0) -> sync_with_stdio(0);
file(finale);
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> a[i];
}
memset(dp, 0xc0, sizeof dp);
dp[0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = i; j >= 1; j--) {
dp[j] = max(dp[j], dp[j-1] + j*a[i]);
}
}
for (int i = 1; i <= n; i++) {
cout << dp[i] << ' ';
}
return 0;
}
双指针练习
-
leetcode 713. 乘积小于 K 的子数组
给定长度为 \(n\) 的正整数数组 \(a\) 和整数 \(k\),找出该数组内乘积小于 \(k\) 的连续子数组的个数。\(1 \le n \le 3 \times 10^4\)。
简析 答案显然具有单调性,因此维护指针 \(l,r\) 初始均指向 \(1\),维护当前乘积 \(prod\),先向右移动 \(r\),当 \(tmp \ge k\) 时固定 \(r\) 右移 \(l\),直到 \(tmp < k\),重复以上过程。由于每个指针只走了一遍数组,所以时间复杂度 \(O(n)\)。
-
洛谷 P3066 Running Away From the Barn G
给定一棵 \(n\) 个点的有根树(\(1\) 为根),边有边权,给定参数 \(t\),对于树上的每个节点 \(u\),求出 \(u\) 子树中有多少节点满足该节点到 \(u\) 的距离不大于 \(t\)。\(1 \le n \le 2\times 10^5\)。
简析 边 DFS 边维护一个栈,可以在栈中求出所有节点到 \(1\) 的路径是什么。对栈末节点 \(u\) 二分找到距离它不大于 \(t\) 的节点中深度最小的节点 \(p\),然后对于 \(u\) 到 \(p\) 路径上所有的节点都会跟 \(u\) 产生一个合法数对,树上差分即可。(似乎跟双指针关系不大)
-
leetcode 524. 通过删除字母匹配到字典里最长单词
给定目标字符串 \(s\) 和字符串数组 \(d\) 作为字典,找出并返回字典中最长的字符串,该字符串可以通过删除 \(s\) 中的某些字符得到。
简析 遍历字典 \(d\) 中的每个字符串 \(t\),问题变为:给定字符串 \(s\) 和 \(t\),判断 \(t\) 是否为 \(s\) 的子序列。方法是,指定两个指针 \(i\) 和 \(j\) 分别指向 \(s\) 和 \(t\) 的首位,当 \(s_i = t_j\) 时向前移动 \(i\) 和 \(j\);否则向前移动 \(i\),继续寻找。
-
leetcode 167. 两数之和 II - 输入有序数组
给定升序数组 \(a\),找出两个不同的数使它们的和等于 \(t\)。
简析 指定指针 \(l=1,r=n\),发现 和随 \(l\) 增大而增大,随 \(r\) 减小而减小,具有单调性,可以不断逼近出和的位置。
需要证明正确性。

浙公网安备 33010602011771号