C. Self-Stopping Shower
将按键时间升序并去掉 \(>T\),加入哨兵 \(T\),维护当前覆盖右端 \(r\),遍历每个时间点 \(x\):若 \(r<x\) 则补按 \(m=⌈\frac{x−r}{K}⌉\) 次,随后令 \(r=x+K\),累加所有 \(m\) 即为最少按键次数。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
int n, k, t;
cin >> n >> k >> t;
vector<int> a(n);
rep(i, n) cin >> a[i];
ranges::sort(a);
while (a.size() and a.back() > t) a.pop_back();
a.push_back(t);
n = a.size();
int ans = 0;
int r = 0;
for (int x : a) {
if (r < x) {
ans += (x-r + k-1)/k;
}
r = x+k;
}
cout << ans << '\n';
return 0;
}
D. Concatenation of Prefixes
令 \(S(i) = \sum\limits_{k=1}^i A_k\),\(T(i) = \sum\limits_{j=1}^i S(j)\)
则 \(T(i)\) 就是把前 \(i\) 个完整块 \((A_1), (A_1, A_2), \cdots, (A_1, \cdots, A_i)\) 全部展开后的元素和;再用 \(len(i)\) 表示前 \(i\) 个块的总长度
定义 \(f(r)\) 为 \(B\) 的前 \(r\) 个元素的和:先用二分找出最大的 \(i\) 使得
\(len(i)<r\),那么前 \(i\) 个完整块的和是 \(T(i)\),剩下的 \(r−len(i)\) 个元素正好是前缀 \((A_1,…,A_{r−len(i)})\),其和为 \(S(r−len(i))\),因此
\(f(r)=T(i)+S(r−len(i))\) 。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
int main() {
int n, q;
cin >> n >> q;
vector<ll> a(n);
rep(i, n) cin >> a[i];
vector<ll> s(n+1);
rep(i, n) s[i+1] = s[i]+a[i];
vector<ll> t(n+1);
rep(i, n) t[i+1] = t[i]+s[i+1];
vector<ll> len(n+1);
rep(i, n) len[i+1] = len[i] + i+1;
auto f = [&](ll r) -> ll {
if (r == 0) return 0;
int si = lower_bound(len.begin(), len.end(), r)-len.begin()-1;
ll res = t[si];
res += s[r-len[si]];
return res;
};
rep(qi, q) {
ll l, r;
cin >> l >> r;
ll ans = f(r) - f(l-1);
cout << ans << '\n';
}
return 0;
}
E. Product-Bounded Arithmetic Sequences
枚举等差数列长度 \(l\),只需试到 \(l≤19\),因为 \(20!>10^{18}\),长度 \(≥20\) 时最小乘积已超上界。
对于固定 \(l\) 和首项 \(a\),要统计满足
的正整数公差 \(d \geqslant 1\) 的个数;若 \(d=1\) 都不行(即 \(\prod\limits_{i=0}^{l-1} (a+i) > N\)),则更大的 \(a\) 也不可能,故终止枚举该 \(l\) 下的 \(a\)。
对每个合法的 \(a\) 用二分查找最大的 \(d\) 使得乘积不超 \(N\),乘积判断通过对
\(m=N\) 依次做 \(m \mathbin{\text{/=}} (a+di)\) 的整除检测以防溢出(只要最后
\(m≥1\) 即表明乘积 \(≤N\))。
将每个 \((l,a)\) 对应的最大 \(d\) 累加即为答案。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
int main() {
ll n;
cin >> n;
ll ans = 0;
for (int l = 3; l <= 19; ++l) {
for (ll a = 1;; ++a) {
{
ll m = n;
rep(i, l) m /= a+i;
if (m == 0) break;
}
auto judge = [&](ll d) {
ll m = n;
rep(i, l) m /= a+d*i;
return m >= 1;
};
ll ac = 1, wa = 1e9;
while (abs(ac-wa) > 1) {
ll wj = (ac+wa)/2;
if (judge(wj)) ac = wj; else wa = wj;
}
ans += ac;
}
}
cout << ans << '\n';
return 0;
}
F. Cake Cutting
令 \(W = \frac{S}{M}\)
考虑把蛋糕划分成若干组,使得每组蛋糕的大小之和都是 \(W\) 的倍数。
假设最多可以划分成 \(K\) 组,那么答案就是
因为一组蛋糕总大小若为 \(qW\),就可以分给 \(q\) 个人,最多需要 \(q-1\) 次切割。反过来,不同组之间无需混合,而每个相互独立的组至少需要 \(q-1\) 次切割,因此最大化组数即可使切割次数最少。
预处理所有子集合 \(\text{sum}[s]\)
考虑状压 \(\text{dp}\),记 \(\text{dp}[s]\) 表示选中集合 \(s\) 内能凑出的和为 \(W\) 的组数的最大值
最后的答案为 \(M-\text{dp}[2^n-1]\)
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
inline void chmax(int& a, int b) { if (a < b) a = b; }
int main() {
int n, m;
cin >> n >> m;
vector<int> a(n);
rep(i, n) cin >> a[i];
int n2 = 1<<n;
vector<ll> sum(n2);
rep(i, n) {
rep(j, 1<<i) sum[j|1<<i] = sum[j]+a[i];
}
ll w = sum[n2-1]/m;
vector<int> dp(n2);
rep(s, n2) {
rep(i, n) if (~s>>i&1) {
int ns = s|1<<i;
chmax(dp[ns], dp[s]+!(sum[ns]%w));
}
}
cout << m-dp[n2-1] << '\n';
return 0;
}
G. Zonal Score Minimization
设 \(dp[i]\) 表示前 \(i\) 个元素 \(A_0,\ldots,A_{i-1}\) 的最小总分,初始化 \(dp[0]=0\),\(dp[1]=\infty\)。
考虑最后一段为 \(A_j,\ldots,A_i\),由于段长至少为 \(2\),必须满足 \(j<i\)。转移为
直接枚举 \(j\) 的复杂度为 \(\mathcal{O}(N^2)\),需要进一步优化。
关键在于维护每个起点的转移候选值。
处理右端点 \(i\) 时,对每个起点 \(j\) 维护
那么 \(dp[i+1]\) 就是所有 \(j<i\) 的 \(v_j\) 的最小值。可以用支持区间加、区间最小值查询和单点赋值的线段树维护这些候选值。
当右端点向右移动、加入新元素 \(A_i\) 时,只需考虑区间最大值和最小值的变化:
- 维护区间最小值: 使用单调递增栈。当新元素 \(A_i\) 小于栈顶值 \(x\) 时,部分区间的最小值由 \(x\) 变成 \(A_i\),对应候选值统一增加 \(x-A_i\)。将这些起点对应的连续区间在另一端的数据结构中整体加上该值。
- 维护区间最大值: 使用单调递减栈。同理,当新元素大于栈顶值 \(x\) 时,对应候选值统一增加 \(A_i-x\),进行区间加。
每次弹栈后,新的栈顶可以确定受到影响的起点范围,因此能够通过一次区间加完成更新。由于每个元素在每个单调栈中最多入栈、出栈各一次,所有弹栈操作总计为 \(\mathcal{O}(N)\)。
处理完当前元素后,通过线段树查询起点范围 \([0,i)\) 的最小值,得到 \(dp[i+1]\),再将该状态作为新的起点加入线段树。
代码实现
#include <bits/stdc++.h>
#include <atcoder/all>
using namespace atcoder;
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
using P = pair<int, int>;
ll op(ll a, ll b) { return min(a, b); }
ll e() { return 1e18; }
ll mapping(ll f, ll x) { return x+f; }
ll composition(ll f, ll g) { return f+g; }
ll id() { return 0; }
int main() {
int n;
cin >> n;
lazy_segtree<ll, op, e, ll, mapping, composition, id> dp(n+1);
dp.set(0, 0);
const int INF = 1001001001;
vector<P> sl = {{-INF, -1}}, sr = {{INF, -1}};
rep(i, n) {
int a;
cin >> a;
while (sl.size() and sl.back().first > a) {
auto [b, r] = sl.back();
sl.pop_back();
int l = sl.back().second;
l++; r++;
dp.apply(l, r, b-a);
}
sl.emplace_back(a, i);
while (sr.size() and sr.back().first < a) {
auto [b, r] = sr.back();
sr.pop_back();
int l = sr.back().second;
l++; r++;
dp.apply(l, r, a-b);
}
sr.emplace_back(a, i);
dp.set(i+1, dp.prod(0, i));
}
cout << dp.get(n) << '\n';
return 0;
}
浙公网安备 33010602011771号