2022 同济大学校赛 题解
A题 盒饭盲盒(签到)
食堂有 \(n\) 种菜,其中 \(a\) 种是素菜,\(n-a\) 种是荤菜。
现在我们去食堂打三份饭,每份饭都会是这 \(n\) 种菜中的一种(不过如果三份菜都是素的话就会重新打),问三份菜都是荤菜的概率有多大?
\(T\leq 1000,1\leq a<n\leq 10^6\)
古典概型,\(p=\dfrac{(n-a)^3}{n^3-a^3}\)。(这个不是很严谨,但是确实是正确的,忘了这个在概率论里面是啥东西了)
#include<bits/stdc++.h>
using namespace std;
#define LL long long
LL gcd(LL a, LL b) { return !b ? a : gcd(b, a % b); }
LL f(LL x) { return x * x * x; }
void solve() {
LL n, a;
cin >> n >> a;
LL A = f(n - a), B = f(n) - f(a);
printf("%lld/%lld\n", A / gcd(A, B), B / gcd(A, B));
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}
C题 攻城 (数学)
给定 \(n\) 个堡垒,第 \(i\) 个堡垒的血量为 \(a_i\)。
现在我们可以不停的进行攻击,每次普通攻击可以选定一个堡垒,对其造成一点伤害。特别的,当攻击次数为 7 的倍数时,这次普通攻击会转化为特殊攻击,他将对所有堡垒(而非某个选定的堡垒)造成一点伤害。
现在,我们想要在某次攻击中一下子摧毁全部堡垒(在这次攻击前,所有堡垒必须仍然存活),问是否可行?
\(n\leq 10^6,1\leq h_i\leq 10^9\)
我们记 \(x\) 为 \(\{a_n\}\) 中的最小值,\(s=\sum\limits_{i=1}^na_i\)。
当 \(n=1\) 时,怼着这唯一一个打就行了。
\(n>1\) 时,意味着我们必须在第 \(7k\) 次消灭所有堡垒,意味着我们要进行 \(6k\) 次普通攻击和 \(k\) 次特殊攻击,那么:
- \(s\) 是 \(n+6\) 的倍数(保证伤害刚刚好)
- \(k\leq x\)(保证不会有堡垒在最后一次攻击前 G 了)
复杂度 \(O(n)\)。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
bool solve() {
LL n, h, sum = 0, Min = 1e9 + 10;
cin >> n;
for (int i = 1; i <= n; ++i) {
cin >> h;
sum += h, Min = min(Min, h);
}
return n == 1 || (sum % (n + 6) == 0 && sum / (n + 6) <= Min);
}
int main()
{
int T;
cin >> T;
while (T--) puts(solve() ? "YES" : "NO");
return 0;
}
D题 两串糖果(线性+区间DP)
给定两串糖果 \(\{a_n\},\{b_n\}\),我们记满意度为 \(\sum\limits_{i=1}^na_ib_i\)。
我们可以选择糖果串 A 上面的若干个区间并进行反转,但是这些区间之间无法重叠,问我们可能使得满意度最大为多少?
\(1\leq n\leq 5*10^3,1\leq a_i,b_i\le 100\)
我们记 \(dp_{i,0}\) 为以 \(i\) 为结尾,且 \(i\) 并非为某区间右端点下的最大满意度(\(dp_{i,1}\) 就是选定 \(i\) 做某个区间右端点),\(f_i=\max(dp_{i,0},dp_{i,1})\),\(g(l,r)\) 为对应区间的反转值(即 \(g(l,r)=\sum\limits_{i=l}^ra_ib_{l+r-i}\)),不难想出 DP 方程:
不过这个 DP 表达式是 \(O(n^2)\) 的,所以必须得把 \(g\) 给压到 \(O(1)\)(也就意味着 \(O(n^2)\) 的空间复杂度和预处理时间)。
-
方法1:枚举中间点
跟判断回文串一样,枚举中间点(注意中间点是一个值还是两个值之间空隙),往两边拓展
-
方法2:区间DP
\[g(i,j)= \begin{cases} g(i+1,j-1)+a_ib_j+a_jb_i&i<j \\ a_ib_i & i=j \\ 0&i>j \end{cases} \]
#include<bits/stdc++.h>
using namespace std;
const int N = 5010;
int n, a[N], b[N];
int st[N][N];
int g(int l, int r) {
if (l > r || st[l][r]) return st[l][r];
if (l == r) return st[l][r] = a[l] * b[l];
return st[l][r] = g(l + 1, r - 1) + a[l] * b[r] + a[r] * b[l];
}
int dp[N][2], f[N];
int main()
{
//read
cin >> n;
for (int i = 1; i <= n; ++i) cin >> a[i];
for (int i = 1; i <= n; ++i) cin >> b[i];
//DP
for (int i = 1; i <= n; ++i) {
dp[i][0] = f[i - 1] + a[i] * b[i];
for (int j = 0; j < i; ++j)
dp[i][1] = max(dp[i][1], f[j] + g(j + 1, i));
f[i] = max(dp[i][0], dp[i][1]);
}
cout << f[n] << endl;
return 0;
}
E题 只想要保底(二分+状压+枚举优化)
给定一个二维矩阵 \(A_{n,m}\),现在我们会随机选择两行 \(i,j\)(可以选取两个相同的行),并构造新数列 \(B_k=\max(A_{i,k},A_{j,k})\)。问,我们应该怎么选,可以使得数列 B 的最小值最大?(要求输出方案,多组方案时选择字典序最小的那种(让 i 尽可能小,然后是 \(j\)))
\(n\leq 5*10^4,m\leq 8,0\leq A_{i,j}\leq 10^9\)
一眼黄焖鸡
最小值最大,那么是典型的二分答案,我们可以直接二分这个最大值 \(x\),然后去写 check。
我们可以让原矩阵中小于 \(x\) 的记为 0,大于等于的记为 1,然后找出两行能互补出全 1 的即可。显然,因为 \(m\leq 8\),所以每一行都可以压成一个 \([0,2^m)\) 之间的整数,然后判断是否存在两个数的或的值为 \(2^m-1\) 即可。
直接 \(O(n^2)\) 寻找显然不行,但是我们发现数的值域极小,所以我们直接在值域上面枚举即可,复杂度降为了 \(O(2^{2m})\)。
对于打印方案,那就是每次标记的时候,\(vis\) 改为存储出现过这个数的最小行即可,然后枚举时候用一个 pair 来不断更新。
总复杂度:\(O(\log 10^9*(nm+2^{2m}))\)。
#include<bits/stdc++.h>
using namespace std;
const int N = 50010;
int n, m, M, a[N][8];
int vis[256];
void build(int val) {
memset(vis, 0, sizeof(vis));
for (int i = 1; i <= n; ++i) {
int x = 0;
for (int k = 0; k < m; ++k)
if (a[i][k] >= val) x |= 1 << k;
vis[x] = vis[x] ? min(vis[x], i) : i;
}
}
auto solve(int x) {
build(x);
auto ans = make_pair(n + 1, n + 1);
for (int i = 0; i < M; ++i)
for (int j = 0; j < M; ++j)
if (vis[i] && vis[j] && (i | j) == M - 1)
ans = min(ans, make_pair(vis[i], vis[j]));
return ans;
}
int main()
{
//read
cin >> n >> m;
M = 1 << m;
for (int i = 1; i <= n; ++i)
for (int j = 0; j < m; ++j)
scanf("%d", &a[i][j]);
//check
int l = -1, r = 1e9 + 10;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (solve(mid).first <= n) l = mid;
else r = mid - 1;
}
//output
auto ans = solve(l);
cout << ans.first << " " << ans.second << endl;
return 0;
}
G题 归零(数据结构)
给定一个长度为 \(n\) 的数列 \(\{a_n\}\) 和 \(m\) 组询问 \((l,r,k)\),每次询问的目标是将子区间 \([l,r]\) 全部变为 0。
我们可以执行两种操作:
- \(a_i=(a_i+1)\% k\)
- \(a_i=(a_i-1)\%k\)
对于每次询问,输出至少需要多少次操作才能达到目标?
\(1\leq n,m\leq 2*10^5,k\leq 10^9,\forall i \forall k\{0\leq A_i<k\}\)
显然,对于某个数 \(x\),当 \(x\leq \lfloor \frac{k}{2}\rfloor\) 时候适合执行操作1,反之执行操作2。也就是说,记 \(f(x)\) 为 \(x\) 所需要的操作次数,有
在线做法:归并树/主席树
我们记一段区间内小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数的和为 \(s_1\),大于的为 \(s_2\),大于的数的个数为 \(n_2\),那么答案为 \(s_1+n_2k-s_2\)。
\(s_1\) 可以表示为区间所有元素的和(这个可以前缀和写)减去 \(s_2\),所以我们需要一个数据结构,来查询:
- 一段区间内大于 \(x\) 的数的个数
- 一段区间内大于 \(x\) 的数的和
主席树可以解决这个问题,但我不是很会。考虑到本题不带修,所以我们用另一个方法:归并树。
归并树类似线段树,不过每个节点存储的都是对应区间内所有元素排好序的结果(所以很占空间,空间复杂度也是 \(O(n\log n)\))。建立流程类似线段树和归并排序的合体(整体框架是线段树,pushup是归并排序)。
void build(int d, int l, int r) {
if (l == r) { Merge[d][l] = a[l]; return; }
int mid = (l + r) >> 1;
build(d + 1, l, mid);
build(d + 1, mid + 1, r);
//mergesort
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r)
Merge[d][k++] = Merge[d + 1][Merge[d + 1][i] < Merge[d + 1][j] ? i++ : j++];
while (i <= mid) Merge[d][k++] = Merge[d + 1][i++];
while (j <= r ) Merge[d][k++] = Merge[d + 1][j++];
}
那么,构造完毕后,这两个操作都便于解决了:操作1的话只要找到那几个对应区间,每个区间都 upper_bound 一下,然后根据下表来统计数量即可;对于操作2,则也维护一个前缀和即可,单次操作的复杂度为 \(O(\log^2 n)\)。
总复杂度为 \(O(n\log n+m\log^2 n)\),有点小卡,但是能过。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 200010;
int n, m;
LL a[N], s[N];
//MergeTree
LL Merge[21][N], ms[21][N];
void build(int d, int l, int r) {
if (l == r) { Merge[d][l] = a[l]; return; }
int mid = (l + r) >> 1;
build(d + 1, l, mid);
build(d + 1, mid + 1, r);
//mergesort
int i = l, j = mid + 1, k = l;
while (i <= mid && j <= r)
Merge[d][k++] = Merge[d + 1][Merge[d + 1][i] < Merge[d + 1][j] ? i++ : j++];
while (i <= mid) Merge[d][k++] = Merge[d + 1][i++];
while (j <= r ) Merge[d][k++] = Merge[d + 1][j++];
}
LL Query1(int l, int r, LL V, int L = 1, int R = n, int d = 0) {
if (l <= L && R <= r) {
int P = upper_bound(Merge[d] + L, Merge[d] + R + 1, V) - Merge[d];
return R - P + 1;
}
int mid = (L + R) >> 1;
LL ans = 0;
if (l <= mid) ans += Query1(l, r, V, L, mid, d + 1);
if (r > mid) ans += Query1(l, r, V, mid + 1, R, d + 1);
return ans;
}
LL Query2(int l, int r, LL V, int L = 1, int R = n, int d = 0) {
if (l <= L && R <= r) {
int P = upper_bound(Merge[d] + L, Merge[d] + R + 1, V) - Merge[d];
return ms[d][R] - ms[d][P - 1];
}
int mid = (L + R) >> 1;
LL ans = 0;
if (l <= mid) ans += Query2(l, r, V, L, mid, d + 1);
if (r > mid) ans += Query2(l, r, V, mid + 1, R, d + 1);
return ans;
}
//
int main()
{
//read
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i)
scanf("%lld", &a[i]);
//init
for (int i = 1; i <= n; ++i)
s[i] = s[i - 1] + a[i];
build(0, 1, n);
for (int d = 20; d >= 0; d--)
for (int i = 1; i <= n; ++i)
ms[d][i] = ms[d][i - 1] + Merge[d][i];
//query
while (m--) {
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
printf("%lld\n", Query1(l, r, k / 2) * k + (s[r] - s[l - 1]) - 2 * Query2(l, r, k / 2));
}
return 0;
}
离线做法
直接将询问离线,按照 \(k\) 的大小来排序,然后从小到大依次处理。
我们开两个树状数组(大小为 \(n\)),一个存数量一个存值,每当读到一个询问的时候,我们就将所有小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数全部插入树状数组里面,对对应区间直接查询即可得到所有小于等于 \(\lfloor \frac{k}{2}\rfloor\) 的数的个数以及数值之和,随后套公式即可得到答案。
该方案复杂度为 \(O(n\log n+m\log m)\) 规模,复杂度更优,而且常数还小。
#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int N = 200010;
int n, m;
LL presum[N];
struct BIT {
LL a[N];
inline LL lowbit(LL x) { return x & -x; }
void add(int i, LL x) {
for (; i <= n; i += lowbit(i)) a[i] += x;
}
LL ask(LL i) {
LL res = 0;
for (; i; i -= lowbit(i)) res += a[i];
return res;
}
LL query(int l, int r) { return ask(r) - ask(l - 1); }
} t1, t2;
struct Node { int num, id; } nodes[N];
struct Query { int l, r, k, id; } query[N];
LL ans[N];
int main()
{
//read
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d", &nodes[i].num);
nodes[i].id = i;
}
for (int i = 1; i <= m; i++) {
Query &q = query[i];
scanf("%d%d%d", &q.l, &q.r, &q.k);
q.id = i;
}
//init
for (int i = 1; i <= n; ++i)
presum[i] = presum[i - 1] + nodes[i].num;
sort(nodes + 1, nodes + n + 1, [](Node a, Node b) { return a.num < b.num; });
sort(query + 1, query + m + 1, [](Query a, Query b) { return a.k < b.k; });
//solve
for (int i = 1, j = 1; i <= m; i++) {
Query &q = query[i];
int l = q.l, r = q.r, k = q.k;
while (k / 2 >= nodes[j].num && j <= n) {
Node &nd = nodes[j++];
t1.add(nd.id, 1), t2.add(nd.id, nd.num);
}
LL n2 = r - l + 1 - t1.query(l, r), s1 = t2.query(l, r);
LL s2 = presum[r] - presum[l - 1] - t2.query(l, r);
ans[q.id] = (presum[q.r] - presum[q.l - 1] - s2) + n2 * k - s2;
}
//output
for (int i = 1; i <= m; ++i)
printf("%lld\n", ans[i]);
return 0;
}
K题 乐观的R家族(签到)
有 \(n\) 个人参加考试,一共有 \(m\) 个选择题(选择题的答案为 ABCDE 中的某一个),答对了第 \(i\) 题可以获得 \(a_i\) 分。
现在我们知道了每个人的作答(一共 \(n\) 个长度为 \(m\) 的字符串),问他们的总分之和的可能的最大值。
\(1\leq n,m,a_i\leq 10^3\)
对于每一题,选择作答最多的那个选项为正确答案即可,这样可以使得总分最大化。
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m, a[N];
char s[N][N];
int main()
{
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; ++i) scanf("%s", s[i] + 1);
for (int i = 1; i <= m; ++i) scanf("%d", &a[i]);
int res = 0;
for (int i = 1; i <= m; ++i) {
int v[5] = {0, 0, 0, 0, 0};
for (int j = 1; j <= n; ++j) v[s[j][i] - 'A']++;
sort(v, v + 5);
res += v[4] * a[i];
}
cout << res << endl;
return 0;
}