本博客是博主参加的某场 CF 杂题赛的赛后总结。
T1 1622C - Set or Decrease
初步观察题面发现,先不管操作方式,操作得更多肯定不会变差,即操作次数满足单调性,自然地想到二分。
接下来思考如何判断,我们的操作分为两种——减小一个数和复制赋值,通过贪心可以得到,我们肯定是先减少最小的一个数,且复制次数最多为 \(n-1\) ,即将最小的数赋值给其它 \(n-1\) 个数,当复制次数不到 \(n-1\) 时,优先赋值给大的数。
综上,先二分操作次数,将初始就满足特判掉后,二分区间是 \([1,n-1+\min a]\) ,即先将最小值减到 \(0\) ,再赋值给别的所有数,然后暴力枚举复制几次,区间是 \([0,n-1]\) ,总时间复杂度 \(\mathcal O(n\log n)\) 。
代码:
const int N = 200010;
int n;
LL lim;
int a[N];
Marisa check(int c) {
LL sum = 0;
for (int i = 0; i <= min(n - 1, c); ++i) {
sum += a[i];
if (sum - 1LL * (a[n] - c + i) * i + c - i >= lim) return 1;
}
return 0;
}
int main() {
int T; read(T); while (T--) {
read(n, lim);
LL sum = 0;
for (int i = 1; i <= n; ++i) {
read(a[i]);
sum += a[i];
}
sort(a + 1, a + n + 1, greater<>());
lim = sum - lim;
if (lim <= 0) {
puts("0");
continue;
}
int l = 1, r = n - 1 + a[n];
while (l < r) {
int mid = l + r >> 1;
if (check(mid)) r = mid;
else l = mid + 1;
}
printf("%d\n", l);
}
return 0;
}
T2 1394A - Boboniu Chats with Du
看完题面,发现代价只用两种,第一种是只消耗 \(1\) 天,另一种是消耗 \(d+1\) 天,但是如果在最后 \(d\) 天使用第二种,则部分代价会溢出而不被计算,考虑贪心。
先将所有玩笑分成以上两种,每种内部排序,然后最后一天尽量大肯定更优,因为禁言的天数会因为溢出不被计算,所以钦定最后一天选择最大的一个,前面的部分就按 \(d+1\) 分组,多余部分先用只消耗 \(1\) 天的填充,剩下的只消耗 \(1\) 天的每 \(d+1\) 个分成一组,当做消耗 \(d+1\) 的,然后和本来就消耗 \(d+1\) 的混合排序,取前面部分,肯定最优。
代码:
const int N = 100010;
int n, d, lim, n1, n2;
int a1[N], a2[N];
int main() {
read(n, d, lim);
for (int i = 1, x; i <= n; ++i) {
read(x);
if (x <= lim) a1[++n1] = x;
else a2[++n2] = x;
}
sort(a1 + 1, a1 + n1 + 1, greater<>());
sort(a2 + 1, a2 + n2 + 1, greater<>());
if (!n2) {
LL ans = 0;
for (int i = 1; i <= n1; ++i) ans += a1[i];
printf("%lld", ans);
return 0;
}
LL ans = a2[1]; --n;
for (int i = 1; i <= min(n % (d + 1), n1); ++i) ans += a1[i];
priority_queue<LL> Q;
for (int i = 2; i <= n2; ++i) {
Q.emplace(a2[i]);
}
LL sum = 0;
for (int i = n % (d + 1) + 1, c = 0; i <= n1; ++i) {
sum += a1[i];
if (++c == d + 1) {
Q.emplace(sum);
sum = c = 0;
}
}
if (sum) Q.emplace(sum);
for (int i = 1; i <= n / (d + 1) && !Q.empty(); ++i, Q.pop()) ans += Q.top();
printf("%lld", ans);
return 0;
}
T3 274D - Lovely Matrix
要求每一行单调不降,是满足拓扑性的关系,考虑建图跑拓扑排序。
一个比较 naive 的想法是每一行里从小的向大的全都连边,考虑对这个想法进行优化,首先,如果没有相等排序完之后相邻的连边的拓扑序与前者等价,但是如果有相等的话,相同的数之间就没有先后关系,所以假设相邻的两个值都有很多数的话,那么他们之间两两都要连边,时间复杂度就爆炸了。
这里有一个套路,就是建虚点,当两组点之间两两都要连边时,从所有起点向虚点连边,再向所有终点连边,在拓扑上是等价的。
所以每一行分开考虑,以每一行建边,最后跑拓扑,将拓扑序列中所有的虚点扣掉后得到的就是答案序列,如果建出的图有环则无解,拓扑时顺便判断。
代码:
const int N = 100010;
int n, m, c;
int a_[N], inD[N << 1], vis[N << 1];
vector<int> ans;
vector<int> g[N << 1];
Marisa&a(int i, int j) { return a_[m * (i - 1) + j]; }
Reimu solve(int k) {
map<int, vector<int>> mp;
for (int i = 1; i <= m; ++i) {
if (~a(k, i)) mp[a(k, i)].emplace_back(i);
}
if (mp.size() <= 1) return;
for (auto it = mp.begin(); ; ++it) {
auto jt = it;
if (++jt == mp.end()) break;
++c;
for (auto t: it->se) {
g[t].emplace_back(c);
++inD[c];
}
for (auto t: jt->se) {
g[c].emplace_back(t);
++inD[t];
}
}
}
Reimu topsort() {
queue<int> Q;
for (int i = 1; i <= c; ++i) {
if (!inD[i]) Q.emplace(i);
}
while (!Q.empty()) {
int x = Q.front(); Q.pop();
if (x <= m) ans.emplace_back(x);
for (auto y: g[x]) {
if (!--inD[y]) Q.emplace(y);
}
}
}
int main() {
read(n, m);
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= m; ++j) {
read(a(i, j));
}
}
c = m;
for (int i = 1; i <= n; ++i) solve(i);
topsort();
if (ans.size() < m) puts("-1");
else {
for (auto i: ans) printf("%d ", i);
}
return 0;
}
T4 1607H - Banquet Preparations 2
思维难度低于 T5 ,但是代码实现细节多得多。
显然,\(x+y-m\) 不同的两盘菜,永远不可能变成同一盘菜,所以先按 \(x-y+m\) 分组,每组内部再判断能否将菜合并。
我们发现,对于每个组内,最后肉相等的肯定是同一道菜,所以我们将每个菜的肉的区间求出来,问题就转化为选取最少的点使每个区间内至少包含一个点。
将所有区间按左端点排序放入一个 set 中,从前往后扫,如果两个区间有交集,就把这两个区间删去,将它们的交集加入该 set 中,如果两个区间没有交集,就后一个区间不变,前一个区间删去,前一个区间中随意选一个点就是合并得到这个区间的所有区间的答案。
代码:
typedef tuple<int, int, int> Ti3;
typedef tuple<int, int, int, int> Ti4;
const int N = 400010;
int n, m;
int a[N], b[N], c[N], t[N], fr[N];
unordered_map<int, vector<int>> f;
Marisa solve(vector<int>&vec) {
set<tuple<int, int, int>> S;
for (auto i: vec) S.emplace(max(0, a[i] - c[i]), min(a[i], a[i] + b[i] - c[i]), i);
int res = 1;
while (S.size() > 1) {
auto jt = S.begin(), it = jt++;
auto [li, ri, ki] = *it;
auto [lj, rj, kj] = *jt;
if (ri >= lj) {
fr[kj] = ki;
S.erase(it); S.erase(jt);
S.emplace(lj, min(ri, rj), kj);
} else {
++res;
for (; ki; ki = fr[ki]) t[ki] = li;
S.erase(it);
}
}
auto [l, r, k] = *S.begin();
for (; k; k = fr[k]) t[k] = l;
return res;
}
int main() {
int T; read(T); while (T--) {
memset(fr + 1, 0, sizeof(int) * n);
f.clear();
read(n); m = 0;
for (int i = 1; i <= n; ++i) {
read(a[i], b[i], c[i]);
f[a[i] + b[i] - c[i]].emplace_back(i);
m = max(m, a[i] + b[i] - c[i]);
}
int ans = 0;
for (auto [v, vec]: f) ans += solve(vec);
printf("%d\n", ans);
for (int i = 1; i <= n; ++i) printf("%d %d\n", a[i] - t[i], c[i] - (a[i] - t[i]));
}
return 0;
}
T5 1042F - Leaf Sets
整场比赛思维性最强的题,但是代码实现非常简单。
要求将叶节点分组使得每组中叶节点间距离不超过 \(k\) ,虽然题面中的无根树有迷惑性,但简单观察即可发现随意找一个非叶节点作为根节点不影响答案。
接下来思考做法,想到树形 dp 。
对于每一棵子树,我们可以发现,留给它父亲合并用得集合最多一个就够了,因为如果可以和深度最大的叶节点深度更大的集合合并一定不如跟另一个合并更优,所以传给父亲的肯定是子树中最深叶节点最浅的集合。
然后思考子树内合并,发现从小往大合并更优,因为合并完后对于子树外等价于只有该集合中深的叶节点这一个点,而将更浅的点子树外的点合并和与子树内合并是等价的,与深度更大的点合并与与深度更小的点合并是等价的。
综上所述,就得到一个做法,首先随便找到一个根,跑树形 dp ,对于每个子树,将深度小的能合并则合并,将合并出的集合传给父亲,传的时候只用传最深的点的深度就好了。
代码:
const int N = 1000010;
int n, lim, ans;
vector<int> g[N];
Marisa dfs(int x, int fa) {
if (g[x].size() == 1) return ++ans & 0;
vector<int> h;
for (auto y: g[x]) {
if (y == fa) continue;
h.emplace_back(dfs(y, x) + 1);
}
sort(h.begin(), h.end());
int mx = h[0];
for (int i = 1; i < h.size(); ++i) {
if (mx + h[i] > lim) break;
mx = h[i];
--ans;
}
return mx;
}
int main() {
read(n, lim);
for (int i = 1, x, y; i < n; ++i) {
read(x, y);
g[x].emplace_back(y);
g[y].emplace_back(x);
}
int rt = 0;
for (int i = 1; i <= n; ++i) {
if (g[i].size() > 1) {
rt = i;
break;
}
}
dfs(rt, 0);
printf("%d", ans);
return 0;
}