NAC2020
Contents
G. ICPC Camp
题意
给定一个整数 $n$,和两个长度分别为 $p,q$ 的数组,$a_1,a_2,…,a_p$ 和 $b_1,b_2,…,b_q$,以及一个整数 $s$。
我们现在要配对出 $n$ 个互不相同(每个 $a_j, b_k$ 只能出现在一个pair中)的pair $(a_j,b_k)$ 使得 $a_j+b_k \leq s$。
定义第 $i$ 个pair的差值为 $d_i = |a_j-b_k|$,那么我们要让 $D = \max d_i$ 的值最小。
其中,$n,p,q \leq 2 \times 10^5, 0 \leq s,a_i,b_i \leq 10^9$。
题解
一眼二分。先 sort一下 $a,b$。
二分之后呢?
假设当前二分的最大差值为 $x$。
那么对于每一个 $a_i$ 我们都可以找到所有满足条件的 $b_j$ 使得 $|a_i-b_j| \leq x$ 且 $a_i+b_j \leq s$。
这样满足条件的 $b_j$ 一定是sort后的一个连续区间。
那么问题转化为,我们有 $p$ 个这样的区间,如何在每个区间上取一个点,使得这些点互不相同,并使得取的点数尽可能多?
比如 $a_1$ 对应的是 $[b_1,b_1]$,$a_2$ 对应 $[b_1,b_2]$, $a_3$ 对应 $[b_2,b_4]$,则我们获得三个区间 $[1,1],[1,2],[2,4]$。
可以在 $[1,1]$ 内选 1 这个点,$[1,2]$ 内选 2 这个点,$[2,4]$ 内选 3 这个点,所以可以选3个点,答案为3。
我们利用贪心尽可能多选点。
我们先将所有区间按照右端点sort一下,然后每个区间 $[L,R]$ 看从 $L$ 开始,第一个没用过的点是哪个,如果它 $\leq R$,就选它。
怎么快速寻找这个没用过的点?
注意到区间上的点数最多为 $q\leq 2 \times 10^5$ 个,我们先将所有的点 (从 $1$ 到 $q$)加入一个 set,然后在set上 lower_bound()
即可。
最后,如果可选的点数 $\geq n$ 就说明 $x$ 是可行的。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 2e5+5;
int n, p, q, s;
int a[maxn], b[maxn];
bool solve(int x) {
vector<pii> vec;
for (int i = 1; i <= p; i++) {
int L = lower_bound(b+1, b+q+1, a[i]-x) - b;
int R = upper_bound(b+1, b+q+1, min(a[i]+x, s-a[i])) - b-1;
if (L <= R) vec.push_back({L,R});
}
int m = vec.size(), ans = 0;
sort(vec.begin(), vec.end(), [](auto a, auto b) {
return a.second < b.second;
});
set<int> se;
for (int i = 1; i <= q; i++) se.insert(i);
for (auto [L, R] : vec) {
if (se.lower_bound(L) != se.end()) {
int t = *se.lower_bound(L);
if (t <= R) {
se.erase(t);
ans++;
}
}
}
return ans >= n;
}
int main() {
fastio;
cin >> n >> p >> q >> s;
for (int i = 1; i <= p; i++) cin >> a[i];
for (int i = 1; i <= q; i++) cin >> b[i];
sort(a+1, a+p+1);
sort(b+1, b+q+1);
int low = 0, high = 1e9, ans = 1e9+1;
while (low <= high) {
int mid = (low + high) >> 1;
if (solve(mid)) {
ans = mid, high = mid-1;
} else low = mid+1;
}
cout << (ans == 1e9+1 ? -1 : ans) << "\n";
}
J. Lunchtime Name Recall
题意
给定一个集合,一开始它为 $\{n\}$,现在我们有 $m$ 天,第 $i$ 天我们拥有 $a_i$ 个汉堡。
在这一天,我们需要将 $a_i$ 个汉堡全部分出去,对于一个集合中的一个元素,假设它为 $v$,那么我们如果分了 $b (0 \leq b \leq v)$ 个汉堡给这个元素,那么它会分裂成 $b, v-b$ 这两个数。
如何分配汉堡,使得 $m$ 天后,集合中元素 $1$ 的出现次数最大?
其中,$2 \leq n \leq 30, 1 \leq m \leq 10$,$a_i \in [1,n-1]$。
整数拆分
一个正整数 $n$ 的整数拆分是将它拆成一些整数的和,即求:
$$n=r_1+r_2+…+r_k,~~ r_1 \geq r_2 \geq … \geq r_k \geq 1$$
的方案数。
我们设 $f_{i,j}$ 表示将整数 $i$ 恰好拆成 $j$ 个数的方案,则有:
$$f_{i,j} = f_{i-1,j-1} + f_{i-j,j}$$
这里表示考虑最小的数是否为 $1$,如果是,就转移到 $f_{i-1,j-1}$,否则让所有数字减 $1$,得到 $f_{i-j,j}$。
由这个可以计算出 $n=30$ 的时候有 $5604$ 种拆法。
题解
暴搜!复杂度大概可以感性理解是和分拆数有关的。具体复杂度至少为 $5604^2$。
在暴搜时需要一些优化:
- 每天如果我们拥有 $a_i$ 个汉堡,那么等价于我们拥有 $n-a_i$ 个汉堡,所以我们取较小的那一个。
- 如果集合中有多个相同的数 $N_1, N_2 … N_k$ 可以分,那么我们分给 $N_i$ 的汉堡数量一定要 $\geq N_{i+1}$ 的汉堡数量(避免重复计算),当然不同的数字之间并没有任何关联。
- 在每一天分的时候,我们维护两个
vector<int>
,一个叫cur
,一个叫add
,分别代表当前还没有分的数,和已经分好了的数,这样可以避免在同一天分了一个数两次。 - 使用哈希,将当前集合的状态 map 到一个hash value,并且离散化之后由整数拆分知道状态数 $\leq 5604$。
- 如果分汉堡的时候会分出 $0$ 的话,就不分这个值。
- 记得在能加引用的地方,加上引用,否则T飞。
复杂度是 $O($玄学$)$。
代码
#include <bits/stdc++.h>
using namespace std;
const int maxn = 35;
const int maxm = 1e5+55;
const int P = 31;
int n, m, a[maxn], cid = 0;
int dp[12][5700]; // dp[i][S]: 还剩 i 次分的机会,当前状态为 S,最多能区分出几个人
map<ull, int> hash_to_idx; // 哈希值 map 到 index
// map<ull, vector<int>> hash_to_vec; // 大小为 5604
// vis = 1: 更新 hash_to_vec
inline ull vec_to_hash(vector<int>& vec, bool vis) {
sort(vec.begin(), vec.end(), greater<int>());
ull res = 0;
for (int i = 0; i < vec.size(); i++) {
res = res * P + vec[i];
}
if (vis && hash_to_idx.count(res) == 0) {
hash_to_idx[res] = ++cid;
}
return res;
}
// 整数拆分
// 还剩下 cur,上一个拆出来的数是 last (所以这次拆的数 <= last), 现在拆出来的是 vec (vec里面的数字不会再被拆)
// void init(int cur, int last, vector<int>& vec) {
// if (cur == 0) return;
// vec.push_back(cur);
// ull res = vec_to_hash(vec, 1);
// vec.pop_back();
// for (int i = min(cur, last); i >= 1; i--) {
// vec.push_back(i);
// init(cur - i, i, vec);
// vec.pop_back();
// }
// }
int dfs(int i, vector<int> cur);
// i: 第 i 天
// b: 还剩下 b 个汉堡可以用
// cur: 还没有分的组
// add: 已经分好的组 (此时它应该是空的?)
// lim: 如果上一个处理的组和当前 cur.back() 的组大小一致,lim 为上一个处理的组拆分的大小,这个不能比它大;如果大小不一致,lim = n (无限制)
int helper(int i, int b, vector<int>& cur, vector<int>& add, int lim) {
if (b < 0) return 0;
if (cur.size() == 0) {
if (b == 0) return dfs(i+1, add);
return 0; // b > 0, 状态不合法
}
// 还剩下没分的组,开始分
int v = cur.back();
cur.pop_back();
bool has_lim = (cur.size() && cur.back() == v); // 下一个是否有限制
int res = 0; // 记录最大值
for (int j = 0; j <= min({b, lim, v}); j++) { // 用掉 j 个汉堡
if (j > 0)
add.push_back(j);
if (j != v)
add.push_back(v-j);
res = max(res, helper(i, b-j, cur, add, has_lim ? j : n));
add.pop_back();
if (j > 0 && j != v)
add.pop_back();
if (res == n) {
cout << n << "\n";
exit(0);
}
}
cur.push_back(v);
return res;
}
// i: 第 i 天 (这一天还没开始分)
// cur: 还没有分的组
// dfs 函数默认是这一天的开始,所以拥有 a[i] 个汉堡可以分
int ans = 0;
int dfs(int i, vector<int> cur) {
ull S = vec_to_hash(cur, 1);
int s = hash_to_idx[S];
if (dp[i][s] != -1) return dp[i][s];
if (i > m) {
int cnt = 0;
for (int j : cur) cnt += (j == 1);
ans = max(ans, cnt);
return dp[i][s] = cnt;
}
vector<int> add;
return dp[i][s] = helper(i, a[i], cur, add, n); // 记忆化
}
int main() {
cin >> n >> m;
for (int i = 1; i <= m; i++) cin >> a[i], a[i] = min(a[i], n-a[i]);
sort(a+1, a+m+1, greater<int>());
vector<int> vec;
memset(dp, -1, sizeof(dp));
vector<int> cur;
cur.push_back(n);
dfs(1, cur);
cout << ans << endl;
}