2024 暑假友谊赛 1
2024 暑假友谊赛 1
A - 😜
题意
现有 N 道菜需要连续使用烤箱 \(T_i\) 分钟,你有两个烤箱,问你烹饪 N 道菜所需最短时间。
思路
可以猜想一定是 \(\frac{\sum_{i=1}^nT_i}{2}\) 附近,贪心不会,考虑 dp。
用背包 dp 将所有数的拼凑方案表示出来,因为 \(1≤N≤100,1≤T_i≤10^3\),所以最大值不会超过 1e5,然后遍历可以拼凑的方案,更新最小值即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
int sum = 0;
vector<int> T(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> T[i];
sum += T[i];
}
int ans = 1 << 30;
const int N = 1e5;
vector<int> dp(N + 10);
dp[0] = 1;
for (int i = 1; i <= n; i ++) {
for (int j = N; j >= T[i]; j--) {
dp[j] |= dp[j - T[i]];
}
}
for (int i = 0; i <= N; i++) {
if (dp[i]) {
ans = min(ans, max(i, sum - i));
}
}
cout << ans << '\n';
return 0;
}
B - 😭
题意
给你 n 个点,当且仅当 \(x_i<x_j\) 并且 \(y_i<y_j\) 时,\((i,j)\) 可以组成一对,一个点不能被多个点对公用,问最多可以凑成多少对。
思路
前置知识:二分图最大匹配、匈牙利算法。
把凑成一对看成由 i 向 j 连一条边,这道题就是典型的二分图最大匹配模板题,由于直接用的板子,所以没啥细节了。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
struct augment_path {
vector<vector<int> > g;
vector<int> pa; // 匹配
vector<int> pb;
vector<int> vis; // 访问
int n, m; // 两个点集中的顶点数量
int dfn; // 时间戳记
int res; // 匹配数
augment_path(int _n, int _m) : n(_n), m(_m) {
assert(0 <= n && 0 <= m);
pa = vector<int>(n, -1);
pb = vector<int>(m, -1);
vis = vector<int>(n);
g.resize(n);
res = 0;
dfn = 0;
}
void add(int from, int to) {
assert(0 <= from && from < n && 0 <= to && to < m);
g[from].push_back(to);
}
bool dfs(int v) {
vis[v] = dfn;
for (int u : g[v]) {
if (pb[u] == -1) {
pb[u] = v;
pa[v] = u;
return true;
}
}
for (int u : g[v]) {
if (vis[pb[u]] != dfn && dfs(pb[u])) {
pa[v] = u;
pb[u] = v;
return true;
}
}
return false;
}
int solve() {
while (true) {
dfn++;
int cnt = 0;
for (int i = 0; i < n; i++) {
if (pa[i] == -1 && dfs(i)) {
cnt++;
}
}
if (cnt == 0) {
break;
}
res += cnt;
}
return res;
}
};
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<array<int, 2>>red(n), bule(n);
for (auto &[x, y] : red)
cin >> x >> y;
for (auto &[x, y] : bule)
cin >> x >> y;
augment_path ans(n, n);
for (int i = 0; i < n; i ++) {
auto [rx, ry] = red[i];
for (int j = 0; j < n; j ++) {
auto [bx, by] = bule[j];
if (rx < bx && ry < by) {
ans.add(i, j);
}
}
}
cout << ans.solve() << '\n';
return 0;
}
C - 😠
题意
给你 \(n\times m\) 矩阵的单元格,你有 \(\frac{nm}{2}\) 个 1 × 2 的多米诺骨牌,现要求你将其中 k 个水平放置,其余垂直放置,问是否可行。(保证 \(n\times m\) 一定是偶数)
思路
对 n,m 分奇偶讨论:
- n,m 都为偶数时:\(n\times m\) 一定可以拆分成 \(\frac{n}{2}\times \frac{m}{2}\) 个 2 × 2 的正方形,这时只要判断一下 k 是否为偶数即可。
- n 为奇数,m 为偶数:
- 考虑转换为第一种情况。
- 当我们把第 1 行单独填满时,剩下的就和第一种情况一样了,考虑把长度为 m 的一行需要 \(\frac{m}{2}\) 个水平骨牌,所以判断剩下的 \((k-\frac{m}{2})\) 是否为偶数即可,当然负数也不行,否则第一行也填不满。
- n 为偶数,m 为奇数:
- 同样是考虑转换为第一种情况。
- 当我们把这个矩阵竖起来看,它就是第二种情况了,此时需要把长度为 n 的单独填满,但这个时候要求的是 \(\frac{n}{2}\) 个垂直骨牌,而我们有 \(\frac{nm}{2} - k\) 个垂直骨牌,于是就看 \((\frac{nm}{2}-k-\frac{n}{2})\) 是否为偶数即可,当然,负数也不行。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t;
cin >> t;
while (t--) {
int n, m, k;
cin >> n >> m >> k;
if (n % 2 == 0 && m % 2 == 0) {
puts(k & 1 ? "NO" : "YES");
} else if (n & 1) {
int ok = (k - m / 2);
if (ok < 0) puts("NO");
else puts(ok & 1 ? "NO" : "YES");
} else {
int ok = (n * m / 2 - k - n / 2);
if (ok < 0) puts("NO");
else puts(ok & 1 ? "NO" : "YES");
}
}
return 0;
}
D - 😓
题意
给你 3 个序列,你需要从中选出其三元组之和最大的前 k 个。
思路
先预处理出两个序列和最大的前 k 个,因为保证 k 最大只有三千,所以又可以把这 k 个拿去和第三个序列组合,再次选出最大的前 k 个即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int x, y, z, k;
cin >> x >> y >> z >> k;
const int N = 3000;
vector<i64> X(x + 1), Y(y + 1), Z(z + 1);
for (int i = 1; i <= x; i ++)
cin >> X[i];
for (int i = 1; i <= y; i ++)
cin >> Y[i];
for (int i = 1; i <= z; i ++)
cin >> Z[i];
vector<i64> s;
priority_queue<i64, vector<i64>, greater<>> Q;
for (int i = 1; i <= x; i ++) {
for (int j = 1; j <= y; j ++) {
i64 t = X[i] + Y[j];
if (Q.size() < k) {
Q.push(t);
} else if (Q.top() < t) {
Q.pop();
Q.push(t);
}
}
}
vector<i64> XY;
while (Q.size()) {
XY.push_back(Q.top());
Q.pop();
}
for (int i = 0; i < XY.size(); i ++) {
for (int j = 1; j <= z; j ++) {
i64 t = XY[i] + Z[j];
s.push_back(t);
}
}
sort(s.begin(), s.end(), greater<>());
int cnt = 0;
for (auto i : s) {
cout << i << '\n';
if (++cnt >= k) break;
}
return 0;
}
E - 🐔
题意
给你一棵树, Fennec 从 1 开始染色,可以把与黑色点相邻但未被染色的点染成黑色,Snuke 同理,不过是从 n 开始,将点染成白色,开始只有点 1 为黑色,n 为白色, F 先手,问你在双方最优染色方案下,谁会获胜。
思路
对于树上某一个点,如果 F 走到这里路程小于等于 S 走到的这里路程,那么 F 一定可以在 S 先到达这里时染色,等于也可以是因为 F 有先手优势,所有可以用两次 dfs 求出 F 和 S 到达所有点的距离,对于每个点比较双方的路程更新双方能够染色的点,最后比较一下即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector g(n + 1, vector<int>());
for (int i = 1; i < n; i ++) {
int u, v;
cin >> u >> v;
g[u].push_back(v);
g[v].push_back(u);
}
vector dis(2, vector<int>(n + 1));
auto dfs = [&](auto & self, int u, int fa, int id)->void{
for (auto v : g[u]) {
if (v == fa) continue;
dis[id][v] = dis[id][u] + 1;
self(self, v, u, id);
}
};
dfs(dfs, 1, 0, 0);
dfs(dfs, n, 0, 1);
int num0 = 0, num1 = 0;
for (int i = 1; i <= n; i ++) {
if (dis[0][i] <= dis[1][i]) num0 ++;
else num1 ++;
}
puts(num0 > num1 ? "Fennec" : "Snuke");
return 0;
}
F - 🐂
题意
你有一个 \(n\times m\) 的矩阵,定义 \(A_{i,j} = i \times j\),问你第 k 大元素为多少。
思路
1 | 2 | 3 | 4 | 5 | 6 |
---|---|---|---|---|---|
2 | 4 | 6 | 8 | 10 | 12 |
3 | 6 | 9 | 12 | 15 | 18 |
4 | 8 | 12 | 16 | 20 | 24 |
5 | 10 | 15 | 20 | 25 | 30 |
6 | 12 | 18 | 24 | 30 | 36 |
对于这样一个 6 × 6 的表格,如果 k 等于 10 ,那么通过观察可以发现,每行最多只有 \(max(\frac{k}{i},m)\) 个元素比 k 小,又因为对于值越大的元素,小于它的元素是越多的,因此答案满足单调性,二分答案即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 n, m, k;
cin >> n >> m >> k;
auto check = [&](i64 mid)->bool{
i64 res = 0;
for (int i = 1; i <= n; i ++) {
res += min(mid / i , m);
if (res >= k) return true;
}
return res >= k;
};
i64 l = 1, r = n * m, ans = -1;
while (l <= r) {
i64 mid = l + r >> 1;
if (check(mid)) r = mid - 1, ans = mid;
else l = mid + 1;
}
cout << ans << '\n';
return 0;
}
G - 😄
题意
给你两个数 x,y,你可以对 x 进行两种操作任意次:
-
在 x 的二进制后添加 0 ,然后翻转二进制(去除前导零)。
-
在 x 的二进制后添加 1 ,然后翻转二进制(去除前导零)。
问你最后能否变成 y 。
思路
一个数加 0 后翻转,和没加 0 时一样的,因为翻转后都会去除前导零。
然后直接对两种操作进行一个搜索即可,当搜的数 x 大于 y 的两倍时退出即可,以此剪枝,但是这样可能会误判 (8,1)这种数据,但是结合以上结论来看,我们可以再反着搜一次,即对翻转后的 x 再 dfs 一次即可。
注意过程可能爆longlong。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
i64 x, y;
cin >> x >> y;
auto re = [](i64 x)->i64{
i64 res = 0;
vector<int> mi;
while (x) {
mi.push_back(x & 1);
x >>= 1;
}
while (!mi.back()) mi.pop_back();
reverse(mi.begin(), mi.end());
for (int i = 0; i < mi.size(); i ++) {
if (mi[i])
res += (1ll << i);
}
return res;
};
unordered_map<i64, bool> vis;
auto dfs = [&](auto & self, i64 z) {
if (z == y) {
cout << "YES\n";
exit(0);
}
if (vis[z] || (__int128)z >= ((__int128)y << 1)) {
return ;
}
vis[z] = 1;
self(self, re(z));
z <<= 1, z ++;
self(self, re(z));
};
dfs(dfs, x);
dfs(dfs, re(x));
cout << "NO\n";
return 0;
}
H -
题意
一家水果店出售苹果。您可以按照任意顺序多次执行以下操作:
- 用 X 日元(日本的货币)买一个苹果。
- 用 Y 日元买三个苹果。
您需要支付最少多少日元才能获得确切 N 的苹果?
思路
比较下单买一个苹果和一次买三个苹果的价格即可。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int x, y, n;
cin >> x >> y >> n;
if (x * 3 <= y) {
cout << n * x << '\n';
} else {
cout << n / 3 * y + n % 3 * x << '\n';
}
return 0;
}
I - 😚
题意
给定一个序列 𝑎,问将其划分成若干段,满足第 𝑖 段的和是 𝑖 的倍数的划分方案的个数。
思路
考虑 dp。
设 \(dp_{i,j}\) 表示前 i 个数分成 j 段的方案数,可以得到以下转移方程:
复杂度 \(O(n^3)\),显然超时。
考虑优化,用前缀和优化,可以转变为 $(sum_i-sum_k) \bmod j=0 $,即 \(sum_i \equiv sum_k \pmod{j}\)。
设 \(g_{k,j}\) 为当 \(sum_i \bmod j = k\) 时,分成 j 段的方案数,则原式转化为 \(dp_{i,j}=g_{sum_i\bmod j,j-1}\),由于 \(dp_{i,j-1}\)会对 \(g_{sum_{i+1}\bmod j,j-1}\) 产生贡献,所以当我们使用完 \(g_{sum_i\bmod j,j-1}\) 后,需要加上 \(dp_{i,j-1}\)。
枚举方案由于段数是若干段,即未知的,所以应该从段数即 j 开始枚举 n 个数在当前段数下产生的总方案数来进行转移。
代码
#include<bits/stdc++.h>
using namespace std;
using i64 = long long;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
const i64 mod = 1e9 + 7;
int n;
cin >> n;
vector dp(n + 1, vector<i64>(n + 1));
vector g(n + 1, vector<i64>(n + 1));
vector<i64> a(n + 1), pre(n + 1);
for (int i = 1; i <= n; i ++) {
cin >> a[i];
pre[i] = pre[i - 1] + a[i];
}
g[0][0] = 1;
for (int j = 1; j <= n; j ++) {
for (int i = 1; i <= n; i ++) {
auto& sum = g[pre[i] % j][j - 1];
dp[i][j] = sum % mod;
sum = (sum + dp[i][j - 1]) % mod;
}
}
i64 ans = 0;
for (int i = 1; i <= n; i ++)
ans = (ans + dp[n][i]) % mod;
cout << ans << '\n';
return 0;
}