【比赛题解】THUSC2021 简要题解
D1T1. 搬箱子
Description
给出一个长度为 \(n\) 的序列 \(a\),和一个限制值 \(m\)。
每一轮需要取走一些数:
- 在取走的数之和不超过限制值 \(m\) 的前提下,取走的数尽量多。
- 在取走的数尽量多的情况下,取走的数的下标形成的序列的字典序尽量大。
在满足上述取数方案的情况下,一共会进行多少轮?
数据范围:\(1 \leq n \leq 5 \times 10^4\),\(1 \leq m, a_i \leq 10^9\)。
Solution
模拟每一轮取数的过程。在前几轮取数都没有问题的时候,显然贪心地在剩下的数中从最小的数一个一个选起,可以得到该轮取走的数的数目,不妨记这个量为 \(k\)。
然后可以一位一位地确定这 \(k\) 个取走的数的下标。
假设当前已经钦定了前 \(t\) 个取走的数的下标 \(p_1, p_2, \cdots, p_t\)。因为要满足字典序尽量大,这个 \(p_{t + 1}\) 在满足题目要求的条件下一定是越靠后越好。
那么此时选定的 \(p_{t + 1}\) 需要满足:在区间 \([p_{t + 1}, n]\) 中前 \(k - t\) 小的数的和不超过 \(m - \sum\limits_{i = 1}^{t }a_{p_i}\)。
那么二分答案即可。
使用的数据结构需要支持:单点修改 + 后缀前 \(k\) 小查询。可以使用树状数组套权值线段树,树状数组维护后缀。
修改和查询的时间复杂度都是 \(\mathcal{O}(\log^2 n)\),二分答案还有一个 \(\mathcal{O}(\log n)\)。于是总时间复杂度为 \(\mathcal{O}(n \log^3 n)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
#include <set>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 50010, MLOGNLOGN = 20001000;
const int SIZE = 1e9 + 10;
int n, m;
int a[N];
multiset<int> s;
int cT, root[N];
struct SegmentTree {
int lc, rc;
int cnt;
long long sum;
} t[MLOGNLOGN];
int New() {
int p = ++ cT;
t[p].lc = t[p].rc = t[p].cnt = t[p].sum = 0;
return p;
}
void insert(int &p, int l, int r, int delta, int Cv, int Sv) {
if (!p) p = New();
t[p].cnt += Cv, t[p].sum += Sv;
if (l == r) return;
int mid = (l + r) >> 1;
if (delta <= mid)
insert(t[p].lc, l, mid, delta, Cv, Sv);
else
insert(t[p].rc, mid + 1, r, delta, Cv, Sv);
}
void add(int x, int delta, int Cv, int Sv) {
x = n - x + 1;
for (; x <= n; x += x & -x)
insert(root[x], 1, SIZE, delta, Cv, Sv);
}
vector<int> rt;
void prework(int x) {
x = n - x + 1;
for (int i = rt.size() - 1; i >= 0; i --) rt.pop_back();
for (; x; x -= x & -x)
rt.push_back(root[x]);
}
long long ask(int l, int r, int k) {
if (l == r)
return 1ll * k * l;
int mid = (l + r) >> 1;
int lcnt = 0;
long long lsum = 0;
for (int i = 0; i < rt.size(); i ++)
lcnt += t[t[rt[i]].lc].cnt,
lsum += t[t[rt[i]].lc].sum;
if (k <= lcnt) {
for (int i = 0; i < rt.size(); i ++) rt[i] = t[rt[i]].lc;
return ask(l, mid, k);
} else {
for (int i = 0; i < rt.size(); i ++) rt[i] = t[rt[i]].rc;
return ask(mid + 1, r, k - lcnt) + lsum;
}
}
vector<int> recover;
void round() {
int L = m;
int k = 0;
while (s.size() && (*s.begin()) <= L) {
int val = *s.begin();
recover.push_back(val);
s.erase(s.begin());
L -= val;
k ++;
}
for (int i = recover.size() - 1; i >= 0; i --) {
int val = recover[i];
s.insert(val);
recover.pop_back();
}
int res = m;
int last = 0;
for (int t = 1; t <= k; t ++) {
int l = last + 1, r = n;
while (l < r) {
int mid = (l + r + 1) >> 1;
prework(mid);
if (ask(1, SIZE, k - t + 1) <= res) l = mid; else r = mid - 1;
}
last = l;
res -= a[l];
s.erase(s.find(a[l]));
add(l, a[l], -1, -a[l]);
}
}
int main() {
n = read(), m = read();
for (int i = 1; i <= n; i ++)
a[i] = read();
for (int i = 1; i <= n; i ++)
s.insert(a[i]);
for (int i = 1; i <= n; i ++)
add(i, a[i], 1, a[i]);
int ans = 0;
while (s.size())
round(), ans ++;
printf("%d\n", ans);
return 0;
}
D1T2. 白兰地厅的西瓜
Description
给出一棵 \(n\) 个点的树,每个点都有一个权值 \(a_i\)。
可以在树上选定一个起点 \(S\) 和一个终点 \(T\)。
你需要最大化从 \(S\) 到 \(T\) 的简单路径上所有点按顺序组成的权值序列的最长上升子序列的长度。
数据范围:\(1 \leq n \leq 10^5\),\(1 \leq a_i \leq 10^9\)。
Solution
可以枚举 \(S\) 到 \(T\) 的简单路径上最高(深度最小)的那个点 \(p\)。
那么这个简单路径可以被拆成 \(p\) 以及从 \(p\) 开始往下走的两条路径,一条路径包含了 \(S\),一条路径包含了 \(T\)。
分两种情况:
- 答案包含 \(p\):则包含 \(S\) 的链所取出的 LIS 均小于 \(a_p\),包含 \(T\) 的链所取出的 LIS 均大于 \(a_p\),两链内部均递增。
- 答案不包含 \(p\):则必定可以找到一个数 \(H\),使得包含 \(S\) 的链所取出的 LIS 均小于等于 \(H\),包含 \(T\) 的链所取出的 LIS 均大于 \(H\),两链内部均递增。
可以考虑线段树合并。
权值线段树的每个节点上需要维护 \(st, ed\):分别表示以权值在 \([l, r]\) 内的点为起点的 LIS 的最大长度(最长下降链),以权值在 \([l, r]\) 内的点为终点的 LIS 的最大长度(最长上升链)。
对于第一种情况,直接在遍历的时候查询线段树更新即可;第二种情况,需要在线段树合并的时候更新答案。
时间复杂度 \(\mathcal{O}(n \log n)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 100100, MLOGN = 10001000;
const int SIZE = 1e9 + 1;
int n;
int a[N];
int tot, head[N], ver[N * 2], Next[N * 2];
void add(int u, int v) {
ver[++ tot] = v; Next[tot] = head[u]; head[u] = tot;
}
int ans = 1;
int cT, root[N];
struct SegmentTree {
int lc, rc;
int st, ed;
SegmentTree() {
lc = rc = 0;
st = ed = 0;
}
} t[MLOGN];
int New() {
int p = ++ cT;
t[p].lc = t[p].rc = 0;
t[p].st = t[p].ed = 0;
return p;
}
void insert(int &p, int l, int r, int delta, int val, int type) {
if (!p) p = New();
if (type == 1) t[p].st = max(t[p].st, val);
else t[p].ed = max(t[p].ed, val);
if (l == r) return;
int mid = (l + r) >> 1;
if (delta <= mid)
insert(t[p].lc, l, mid, delta, val, type);
else
insert(t[p].rc, mid + 1, r, delta, val, type);
}
SegmentTree ask(int p, int l, int r, int s, int e) {
if (s <= l && r <= e)
return t[p];
int mid = (l + r) >> 1;
SegmentTree self, lc, rc;
if (s <= mid) {
lc = ask(t[p].lc, l, mid, s, e);
self.st = max(self.st, lc.st);
self.ed = max(self.ed, lc.ed);
}
if (mid < e) {
rc = ask(t[p].rc, mid + 1, r, s, e);
self.st = max(self.st, rc.st);
self.ed = max(self.ed, rc.ed);
}
return self;
}
int merge(int p, int q) {
if (!p || !q)
return p ^ q;
ans = max(ans, t[t[p].lc].ed + t[t[q].rc].st);
ans = max(ans, t[t[q].lc].ed + t[t[p].rc].st);
t[p].lc = merge(t[p].lc, t[q].lc);
t[p].rc = merge(t[p].rc, t[q].rc);
t[p].st = max(t[p].st, t[q].st);
t[p].ed = max(t[p].ed, t[q].ed);
return p;
}
void dfs(int u, int fa) {
int St = 0, Ed = 0;
for (int i = head[u]; i; i = Next[i]) {
int v = ver[i];
if (v == fa) continue;
dfs(v, u);
int valS = ask(root[v], 0, SIZE, a[u] + 1, SIZE).st;
int valT = ask(root[v], 0, SIZE, 0, a[u] - 1).ed;
St = max(St, valS);
Ed = max(Ed, valT);
ans = max(ans, ask(root[u], 0, SIZE, 0, a[u] - 1).ed + valS + 1);
ans = max(ans, ask(root[u], 0, SIZE, a[u] + 1, SIZE).st + valT + 1);
root[u] = merge(root[u], root[v]);
}
insert(root[u], 0, SIZE, a[u], St + 1, 1);
insert(root[u], 0, SIZE, a[u], Ed + 1, 2);
}
int main() {
n = read();
for (int i = 1; i <= n; i ++)
a[i] = read();
for (int i = 1; i < n; i ++) {
int u = read(), v = read();
add(u, v), add(v, u);
}
dfs(1, 0);
printf("%d\n", ans);
return 0;
}
D1T3. Emiya 家明天的饭
Description
有 \(n\) 个人,以及 \(m\) 盘菜。
其中,第 \(i\) 个人对第 \(j\) 盘菜的评价为 \(a_{i, j}\)。特别地,如果 \(a_{i, j} = -1\),则表示第 \(i\) 个人不喜欢第 \(j\) 盘菜。
若某个人在餐桌上看到了自己不喜欢的菜,那么他会被气走。
在确定了上菜方案的情况下。若第 \(i\) 个人在场,第 \(j\) 盘菜也在场,那么你就会获得 \(a_{i, j}\) 的收益。
请确定一个上菜方案,使得你获得的收益最大化。
数据范围:\(1 \leq n \leq 20\),\(1 \leq m \leq 10^6\)。
Solution
考虑枚举最后在场的人的集合 \(S\),设在场的人的集合为 \(S\) 的时候的收益为 \(f(S)\)。
因为不容易直接求出所有 \(f\) 值,所以考虑构造一个辅助函数 \(g\),满足:
记录 \(t_j\) 表示不排斥第 \(j\) 盘菜的人的集合,对于第 \(i\) 个人对第 \(j\) 盘菜的评价 \(a_{i, j} \geq 0\):
注意到如果第 \(j\) 道菜最终在场,那么 \(S\) 一定是 \(t_j\) 的子集,即 \(S \subseteq t_j\)。
为了保证贡献正确计算:
- 上述操作的第一步:计算所有不排斥第 \(j\) 盘菜的人的贡献。
- 上述操作的第二步:扣除所有不排斥第 \(j\) 盘菜,且不在场的人的贡献。
得到了 \(g\) 之后,dp 还原 \(f\)。
时间复杂度 \(\mathcal{O}(2^nn + nm)\)。
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
inline int read() {
int x = 0, f = 1; char s = getchar();
while (s < '0' || s > '9') { if (s == '-') f = -f; s = getchar(); }
while (s >= '0' && s <= '9') { x = x * 10 + s - '0'; s = getchar(); }
return x * f;
}
const int N = 21, M = 1001000;
int n, m;
int a[N][M];
int t[M];
int f[1 << N];
int main() {
n = read(), m = read();
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
a[i][j] = read();
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
if (a[i][j] >= 0) t[j] |= (1 << i);
for (int i = 0; i < n; i ++)
for (int j = 1; j <= m; j ++)
if (a[i][j] >= 0) {
f[t[j]] += a[i][j];
f[t[j] ^ (1 << i)] -= a[i][j];
}
for (int i = 0; i < n; i ++)
for (int S = 0; S < (1 << n); S ++)
if (S & (1 << i)) f[S ^ (1 << i)] += f[S];
int ans = 0;
for (int S = 0; S < (1 << n); S ++)
ans = max(ans, f[S]);
printf("%d\n", ans);
return 0;
}
D1T4. 种树
Description
这是一道通信题。
你需要实现两个函数 encode
、decode
。
encode
函数:接受一棵树的读入,将一个长度为 \(128\) 的 \(0/1\) 串发送给decode
函数。decode
函数:接受一个长度为 \(128\) 的 \(0/1\) 串,将接受到的串还原成原树。
还原出的树和给出的树形态相同即可,不要求点的标号也相同。
数据范围:数据组数 \(T \leq 10^5\),\(n \leq 70\)。
Solution
算法一
部分分:\(n \leq 65\)。
一个朴素的构造:
encode
:钦定任意一个点为根开始 DFS,如果向儿子走则填 \(0\),向父亲走则填 \(1\)。decode
:按顺序处理每一位字符,遇到 \(0\) 则向一个新建的儿子走,遇到 \(1\) 则向父亲走。
算法二
如果将「算法一」构造中的 \(0/1\) 看成是左右括号,则 encode
出来的串一定是合法括号串。
一个大小为 \(n\) 的树 encode
出的合法括号串共有 \(\mathrm{Cat}_{n - 1}\) 种。
注意到 \(\mathrm{Cat}_{69} < 2^{128}\),于是我们应该构造一种将括号串与 \([0, 2^{128})\) 中的数一一对应的映射方式。
字典序就是一个很好的映射方式,dp 处理即可。