C. Adjacent Sums (easy)
当 \(M=2\) 时,每次在某个位置加 \(1\) 就是把该位置的二元值取反,所以我们只关心每个位置的奇偶(模 2)。约束
等价于
因此一旦确定了 \(A_1\) 的模 \(2\) 值,整个满足约束的目标序列就唯一确定了:用递推 \(x_{i+1}=x_i \oplus B_i\) 得到期望的每个位置的值。目标是把原序列变成这个目标序列,代价就是不同位的个数(每处只需翻转一次即可),即海明距离。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
int main() {
int n, m;
cin >> n >> m;
vector<int> a(n), b(n-1);
rep(i, n) cin >> a[i];
rep(i, n-1) cin >> b[i];
int ans = n;
rep(s, 2) {
int now = 0;
int x = s;
rep(i, n) {
if (a[i] != x) now++;
if (i != n-1) x ^= b[i];
}
ans = min(ans, now);
}
cout << ans << '\n';
return 0;
}
D. Concentric Circles
对于一对点 \(P,Q\),所有满足 \(∣OP∣=∣OQ∣\) 的点 \(O\) 构成 \(PQ\) 的垂直平分线。垂直平分线可以用“中点 + 垂直方向向量”表示:中点是 \(\frac{P+Q}{2}\),垂直方向是把向量 \(P−Q\) 旋转 \(90^{∘ }\) 得到的向量。
同理,对 \(R,S\) 得到另一条垂直平分线:中点 \(\frac{R+S}{2}\),方向是 \(R−S\) 旋转 \(90^{∘}\) 。
问题等价于判断这两条直线相交:
- 若方向向量不共线(即向量叉积非 \(0\)),两直线必相交 \(\to\) 存在公共圆心。
- 若方向向量共线(平行),则检查两条直线是否重合:等价于中点连线与方向向量共线(即方向向量与 \(b-a\) 的叉积为 \(0\)),若重合则有无限个交点;否则平行但不同直线。
代码实现
#include <bits/stdc++.h>
#define rep(i, n) for (int i = 0; i < (n); ++i)
using namespace std;
using ll = long long;
// Vector
struct V {
ll x, y;
V(ll x=0, ll y=0): x(x), y(y) {}
V& operator+=(const V& v) { x += v.x; y += v.y; return *this;}
V operator+(const V& v) const { return V(*this) += v;}
V& operator-=(const V& v) { x -= v.x; y -= v.y; return *this;}
V operator-(const V& v) const { return V(*this) -= v;}
V& operator*=(ll s) { x *= s; y *= s; return *this;}
V operator*(ll s) const { return V(*this) *= s;}
ll dot(const V& v) const { return x*v.x + y*v.y;}
ll cross(const V& v) const { return x*v.y - v.x*y;}
ll norm2() const { return x*x + y*y;}
V rotate90() const { return V(y, -x);}
};
istream& operator>>(istream& is, V& v) {
is >> v.x >> v.y; return is;
}
ostream& operator<<(ostream& os, const V& v) {
os<<"("<<v.x<<","<<v.y<<")"; return os;
}
bool solve() {
V p, q, r, s;
cin >> p >> q >> r >> s;
V a = p+q, b = r+s;
V v = (p-q).rotate90(), w = (r-s).rotate90();
if (v.cross(w) != 0) return true;
if (v.cross(b-a) == 0) return true;
return false;
}
int main() {
int t;
cin >> t;
while (t--) {
if (solve()) puts("Yes");
else puts("No");
}
return 0;
}
E. Adjacent Sums (hard)
假设我们最终决定让 \(A_0\) 增加 \(x\) 次(由于是在模 \(M\) 意义下,我们只需要考虑 \(0 \le x < M\))。
根据题目条件:\((A'_i + A'_{i+1}) \bmod M = B_i\),只要第一个元素 \(A'_0\) 的值确定了,后面所有元素的最终目标值(在模 \(M\) 意义下)就全部被唯一确定了。
如果我们不进行任何操作(即 \(x=0\)),我们可以递推计算出每个位置的“基准目标值” \(C_i\):
- \(C_0 = A_0\)
- \(C_{i} = (B_{i-1} - C_{i-1}) \bmod M \quad (\text{对于 } i \ge 1)\)
现在,如果我们让 \(A_0\) 额外增加 \(x\),根据正负号的交替传播规律,每个位置最终的目标值 \(A'_i \pmod M\) 将会变成:
- 当 \(i\) 为偶数时:\(A'_i \equiv C_i + x \pmod M\)
- 当 \(i\) 为奇数时:\(A'_i \equiv C_i - x \pmod M\)
因为每个元素只能增加,所以每个位置实际的操作次数 \(\Delta_i = A'_i - A_i\) 必须是非负整数,且是满足同余方程的最小值:
- 当 \(i\) 为偶数时:$$\Delta_i = (C_i + x - A_i) \bmod M = (x - (A_i - C_i)) \bmod M$$我们记 \(p_i = (A_i - C_i) \bmod M\),则该位置的代价为 \((x - p_i) \bmod M\)。
- 当 \(i\) 为奇数时:$$\Delta_i = (C_i - x - A_i) \bmod M = ((C_i - A_i) - x) \bmod M$$我们记 \(q_i = (C_i - A_i) \bmod M\),则该位置的代价为 \((q_i - x) \bmod M\)。
我们要最小化的总代价函数就是:
如果把 \(f(x)\) 想象成一条几何曲线,随着 \(x\) 从 \(0\) 连续增长到 \(M-1\):
- 对于偶数位置的项 \((x - p_i) \bmod M\):随着 \(x\) 增大,它以斜率 \(+1\) 线性递增;但当 \(x\) 刚刚恰好超过 \(p_i\) 时,该项会瞬间从 \(M-1\) 跌落到 \(0\)(相当于骤减 \(M\))。
- 对于奇数位置的项 \((q_i - x) \bmod M\):随着 \(x\) 增大,它以斜率 \(-1\) 线性递减;但当 \(x\) 离开 \(q_i\) 的瞬间(即 \(x\) 大于 \(q_i\)),该项会瞬间从 \(0\) 跳跃到 \(M-1\)(相当于骤增 \(M\))。
既然函数在绝大多数地方都是平滑的线性变化,最值必然在那些发生骤变的事件点处取得。
代码实现
#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, m;
cin >> n >> m;
vector<int> a(n), b(n-1);
rep(i, n) cin >> a[i];
rep(i, n-1) cin >> b[i];
auto f = [&](int x) {
return (x%m+m)%m;
};
vector<int> p, q, xs;
{
int c = 0;
rep(i, n) {
if (i%2 == 0) p.push_back(f(a[i]-c));
else q.push_back(f(c-a[i]));
if (i != n-1) c = f(b[i]-c);
}
xs = p; xs.insert(xs.end(), q.begin(), q.end());
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(), xs.end()), xs.end());
if (xs[0] == 0) xs.erase(xs.begin());
}
ll now = 0;
map<int, int> pc, qc;
for (int c : p) now += f(-c), pc[c]++;
for (int c : q) now += f(c), qc[c]++;
ll ans = now;
ll px = 0;
for (int x : xs) {
now += (ll)m*qc[px];
now += ll(p.size()-q.size())*(x-px);
px = x;
now -= (ll)m*pc[x];
ans = min(ans, now);
}
cout << ans << '\n';
return 0;
}
F. Email Scheduling Optimization
假设当前所有公司的参数固定。我们需要决定发送邮件的顺序。
利用微扰法(交换论证):考虑紧邻的两个公司 \(x\) 和 \(y\)。如果先发 \(x\) 再发 \(y\),它们两个带来的最大总时间消耗相关项为 \(\max(A_x + B_x, A_x + A_y + B_y)\)。同理,若调换顺序则为 \(\max(A_y + B_y, A_y + A_x + B_x)\)。
可以证明,当 \(B_x > B_y\) 时,先发 \(x\) 总是优于或等于先发 \(y\)。
结论:最优策略是将所有公司按照 \(B_i\) 从大到小排序并依次发送邮件。
若我们将公司按照 \(B_i\) 从小到大重新编号为 \(1, 2, \dots, N\),那么写邮件的顺序就是从 \(N\) 到 \(1\)。此时,第 \(i\) 个公司收到回信的时间为:
我们要求的目标就是 \(\max\limits_{1 \le i \le N} T_i\)。
由于存在修改操作,每次重新排序计算的时间复杂度为 \(O(N \log N)\),总复杂度 \(O(QN \log N)\) 会超时。我们需要动态维护上述公式的最大值。
-
步骤一:离散化值域
把所有初始的 \(B_i\) 以及所有询问中改动的目标值 \(x\) 统一放入一个数组中进行排序去重。把每个可能遇到的 \(B\) 值映射为一个 \([1, N+Q]\) 之间的整数标号。
-
步骤二:线段树区间操作
构建一棵线段树,其每个位置 \(p\) 维护当前位于该 \(B\) 值档位的公司的 \(T_p\) 值。线段树需要支持区间加减和查询全局最大值:
- 当增加/修改一个公司 \(i\) 时:它会对所有 \(B\) 值比它小或相等的项产生影响。假设它离散化后的位置是 \(pos\),我们在线段树的单点 \(pos\) 处激活并赋予初始值 \(B_i\),然后对区间 \([1, pos]\) 统一加上 \(A_i\)。
- 当删除一个公司 \(i\) 时:执行相反操作,对区间 \([1, pos]\) 统一减去 \(A_i\),并将单点 \(pos\) 移出(设为 \(-\infty\))。
对于每个询问,如果是修改 \(A_i\),就先在旧位置扣除旧的 \(A_i\),加上新的 \(A_i\);如果是修改 \(B_i\),就将该公司从旧的离散化位置彻底删除,并插入到新的离散化位置。修改完成后,线段树的根节点维护的即为当前的最小总耗时。
代码实现
#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>;
// Coodinate Compression
template<typename T=int>
struct CC {
bool initialized;
vector<T> xs;
CC(): initialized(false) {}
void add(T x) { xs.push_back(x);}
void init() {
sort(xs.begin(), xs.end());
xs.erase(unique(xs.begin(),xs.end()),xs.end());
initialized = true;
}
int operator()(T x) {
if (!initialized) init();
return upper_bound(xs.begin(), xs.end(), x) - xs.begin() - 1;
}
T operator[](int i) {
if (!initialized) init();
return xs[i];
}
int size() {
if (!initialized) init();
return xs.size();
}
};
const ll INF = 1e18;
ll op(ll a, ll b) { return max(a, b); }
ll e() { return -INF; }
ll mapping(ll f, ll x) { return f+x; }
ll composition(ll f, ll g) { return f+g; }
ll id() { return 0; }
int main() {
int n, q;
cin >> n >> q;
vector<int> a(n), b(n), bid(n);
rep(i, n) cin >> a[i];
rep(i, n) cin >> b[i];
CC<P> cc;
rep(i, n) cc.add(P(b[i], i)), bid[i] = i;
vector<tuple<int, int, int>> qs;
rep(qi, q) {
int type, i, x;
cin >> type >> i >>x;
--i;
qs.emplace_back(type, i, x);
if (type == 2) cc.add(P(x, n+qi));
}
int m = cc.size();
lazy_segtree<ll, op, e, ll, mapping, composition, id> t(m);
auto add = [&](int i) {
int j = cc(P(b[i], bid[i]));
ll d = t.get(j)+INF;
t.set(j, b[i]+d);
t.apply(0, j+1, a[i]);
};
auto del = [&](int i) {
int j = cc(P(b[i], bid[i]));
t.apply(0, j+1, -a[i]);
ll d = t.get(j) - b[i];
t.set(j, d-INF);
};
rep(i, n) add(i);
rep(qi, q) {
auto [type, i, x] = qs[qi];
del(i);
if (type == 1) a[i] = x;
else b[i] = x, bid[i] = n+qi;
add(i);
cout << t.all_prod() << '\n';
}
return 0;
}
G. Many Sweets Problem
树套树板子
浙公网安备 33010602011771号