1 条题解
-
0
二元函数求和最大值问题(Innophone)题解
这道题的核心是通过合理选择常数 (a) 和 (b),最大化二元函数 (f(x,y)) 对所有给定 ((x_i,y_i)) 的求和结果。解题关键在于将函数求和转化为可高效计算的数学模型,并利用排序、离散化和线段树(维护线性函数)优化求解过程。
一、题目分析与核心思路
1. 函数规则拆解
二元函数 (f(x,y)) 的定义可拆解为对每组 ((x_i,y_i)) 的贡献判断:
- 若 (a \leq x_i):贡献为 (a);
- 若 (a > x_i) 且 (b \leq y_i):贡献为 (b);
- 若 (a > x_i) 且 (b > y_i):贡献为 (0)。
2. 求和公式转化
设满足 (a \leq x_i) 的组数为 (c),满足 (a > x_i) 且 (b \leq y_i) 的组数为 (d),则总求和为: [ \sum f(x_i,y_i) = a \cdot c + b \cdot d ] 其中 (c + d \leq n)((c) 是“选 (a) 贡献”的数量,(d) 是“选 (b) 贡献”的数量)。
3. 关键观察
- 若固定 (a),则 (c) 是 (x_i \geq a) 的组数,(d) 是 (x_i < a) 且 (y_i \geq b) 的组数;
- 对固定 (a),(a \cdot c) 是定值,最大化总和只需最大化 (b \cdot d)(即选择 (b) 为某个 (y_i),使 (d) 尽可能大);
- 所有可能的 (a) 必然是某个 (x_i)(因 (x_i) 排序后,(a) 取非 (x_i) 值时,(c) 和 (d) 的计数不变),可通过排序 (x_i) 枚举所有有效 (a)。
二、算法设计与实现步骤
1. 预处理:排序与离散化
- 排序:将所有 ((x_i,y_i)) 按 (x_i) 升序排列,确保枚举 (a) 时可按顺序更新 (c)((x_i \geq a) 的数量);
- 离散化:因 (y_i) 范围可达 (10^9),将 (y_i) 映射到小范围(1~tot,tot 为不同 (y_i) 的数量),方便线段树维护。
2. 线段树维护线性函数
线段树的核心作用是维护“(b \cdot d)”的最大值:
- 每个节点存储一个线性函数 (g(b) = k \cdot b),其中 (k) 是“(y_i \geq b) 且未被 (a) 覆盖”的组数(即 (d));
- 当枚举 (a) 时,若某个 (x_i < a),则该 (x_i) 对应的 (y_i) 需纳入 (b) 的计数范围,即在线段树中更新对应 (y_i) 位置的 (k)(+1);
- 线段树支持快速查询当前所有 (b) 对应的 (g(b)) 最大值(即 (b \cdot d) 的最大值)。
3. 枚举与计算
按排序后的 (x_i) 枚举所有可能的 (a)(即 (a = x_i)):
- 计算当前 (a) 对应的 (a \cdot c)((c = n - i + 1),因前 (i-1) 个 (x_i < a));
- 加上线段树查询到的 (b \cdot d) 最大值,更新全局答案;
- 将当前 (x_i) 对应的 (y_i) 纳入线段树(更新 (k)),为后续更小的 (a) 做准备。
三、完整代码实现
#include <bits/stdc++.h> using namespace std; #define endl '\n' #define ll long long #define pr pair<line, ll> #define mk make_pair #define fst first #define snd second // 快读优化 char buf[1048576], *p1 = buf, *p2 = buf, ubuf[1048576], *u = ubuf; #define getchar() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1048576,stdin),p1==p2)?EOF:*p1++) const ll N = 1.5e5 + 5, inf = LONG_LONG_MAX; ll n, m, tg[N << 2], lsh[N], tot, ans, now; // tg:线段树懒标记;lsh:y的离散化数组 // 线性函数结构体:g(b) = a*b + b(此处变量名注意:a是系数,b是常数项) struct line { ll a, b; friend line operator+(line a, line b) { // 函数合并(系数和常数项分别相加) return {a.a + b.a, a.b + b.b}; } }; // 比较两个线性函数,返回在当前b下更大的函数及交点(交点后另一个函数更优) inline pr maxf(line x, line y) { if (x.a < y.a || (x.a == y.a && x.b < y.b)) swap(x, y); if (x.b >= y.b) // x始终更优,交点为无穷大 return mk(x, inf); // 计算交点:x.a*b + x.b = y.a*b + y.b → b = (y.b - x.b)/(x.a - y.a) return mk(y, (y.b - x.b) / (x.a - y.a)); } // 线段树节点结构体:存储线性函数、有效区间(交点前当前函数更优) struct KTT { line s; // 节点维护的线性函数 ll intr; // 有效区间的右边界(超过后需拆分) #define s(z) t[z].s #define intr(z) t[z].intr // 节点合并:合并左右子节点的函数和有效区间 friend KTT operator+(KTT x, KTT y) { KTT res; pr g; res.intr = min(x.intr, y.intr); // 初始有效区间取两者最小值 g = maxf(x.s, y.s); // 比较两个函数,取当前更优的 res.s = g.fst; res.intr = min(res.intr, g.snd); // 有效区间更新为更小的交点 return res; } } t[N << 2]; // 线段树pushup:合并子节点信息到父节点 inline void pushup(int k) { t[k] = t[k << 1] + t[k << 1 | 1]; } // 线段树建树:初始化每个叶子节点为y离散化后对应的线性函数g(b)=1*b(k=1) inline void build(ll k, ll l, ll r) { if (l == r) { intr(k) = inf; // 叶子节点无交点,有效区间无穷大 s(k) = {lsh[l], 0}; // 初始函数:系数为lsh[l](b的取值),常数项0 return; } ll mid = l + r >> 1; build(k << 1, l, mid); build(k << 1 | 1, mid + 1, r); pushup(k); } // 线段树push:懒标记下传,更新函数和有效区间 inline void push(ll k, ll v) { tg[k] += v; // 懒标记累加(表示b的偏移量) intr(k) -= v; // 有效区间右边界随b偏移减小 s(k).b += s(k).a * v; // 函数常数项更新:a*b中的b偏移v,等价于常数项+a*v } // 线段树pushdown:将懒标记下传到子节点 inline void pushdown(ll k) { if (!tg[k]) return; push(k << 1, tg[k]); push(k << 1 | 1, tg[k]); tg[k] = 0; } // 线段树rebuild:拆分无效节点(有效区间小于0时,需重新合并子节点) inline void rebuild(ll k) { if (intr(k) >= 0) return; // 有效区间仍合法,无需拆分 pushdown(k); // 下传懒标记 rebuild(k << 1); // 递归处理子节点 rebuild(k << 1 | 1); pushup(k); // 重新合并子节点信息 } // 线段树区间更新:将[1, a[r].snd](y<=当前y的范围)的函数系数+1(增加一个计数d) inline void Add(ll k, ll x, ll y, ll v, ll l, ll r) { if (x <= l && r <= y) { push(k, v); rebuild(k); return; } ll mid = l + r >> 1; pushdown(k); if (mid >= x) Add(k << 1, x, y, v, l, mid); if (mid + 1 <= y) Add(k << 1 | 1, x, y, v, mid + 1, r); pushup(k); } // 线段树区间查询:查询[1, tot]的最优线性函数(即b*d的最大值) inline KTT que(ll k, ll x, ll y, ll l, ll r) { if (x <= l && r <= y) return t[k]; ll mid = l + r >> 1; pushdown(k); if (y <= mid) return que(k << 1, x, y, l, mid); if (mid < x) return que(k << 1 | 1, x, y, mid + 1, r); return que(k << 1, x, y, l, mid) + que(k << 1 | 1, x, y, mid + 1, r); } // 快读函数 inline ll read() { ll x = 0, f = 1; char ch = getchar(); for (; !isdigit(ch); ch = getchar()) if (ch == '-') f = -1; for (; isdigit(ch); ch = getchar()) x = (x << 3) + (x << 1) + ch - '0'; return x * f; } pair<ll, ll> a[N]; // 存储(x_i, y_i) int main() { ios::sync_with_stdio(0); cout.tie(0); cin.tie(0); n = now = read(); // now初始为n(c的初始值:所有x_i >= a) // 读入数据并收集y_i用于离散化 for (ll i = 1; i <= n; i++) { a[i].fst = read(); // x_i lsh[++tot] = a[i].snd = read(); // y_i,同时存入lsh } // 排序与离散化 sort(a + 1, a + n + 1); // 按x_i升序排列 sort(lsh + 1, lsh + tot + 1); // 对y_i排序 tot = unique(lsh + 1, lsh + tot + 1) - lsh - 1; // 去重,得到离散化后的数据量 for (ll i = 1; i <= n; i++) { // 将y_i映射到离散化后的索引(1~tot) a[i].snd = lower_bound(lsh + 1, lsh + tot + 1, a[i].snd) - lsh; } // 初始化线段树 build(1, 1, tot); // 枚举所有可能的a(即x_i),计算最大总和 for (ll i = 1, r; i <= n; i++) { // 当前a = a[i].fst,总和 = a*c + (b*d)最大值(线段树维护的s(1).b) ans = max(ans, now * a[i].fst + s(1).b); // 处理所有x_i = a[i].fst的组(避免重复枚举相同a) for (r = i; r <= n && a[i].fst == a[r].fst; r++) { now--; // c减少1(该组x_i = a,后续a更小则x_i < a,不再计入c) // 将该组的y_i纳入线段树(更新d的计数) Add(1, 1, a[r].snd, 1, 1, tot); } i = r - 1; // 跳过已处理的相同x_i的组 } // 输出最大总和 cout << ans; return 0; }四、算法复杂度分析
-
时间复杂度:
- 排序与离散化:(O(n\log n))(排序 (x_i) 和 (y_i),离散化去重);
- 线段树操作:建树 (O(tot))(tot 是不同 (y_i) 的数量,(tot \leq n)),每次更新和查询 (O(\log tot)),共 (O(n\log n)) 次操作;
- 总时间复杂度:(O(n\log n)),可处理 (n=1.5 \times 10^5) 的数据规模。
-
空间复杂度:
- 线段树占用 (O(4 \times tot)) 空间,离散化数组占用 (O(n)) 空间;
- 总空间复杂度:(O(n)),符合题目内存要求。
五、关键优化点
- 离散化:将大范围的 (y_i) 映射到小范围,避免线段树因数值过大导致空间浪费;
- 线段树维护线性函数:将“(b \cdot d)”的最大值问题转化为线性函数的维护,通过合并函数和有效区间实现高效查询;
- 批量处理相同 (x_i):避免对相同 (a) 的重复枚举,减少冗余计算;
- 懒标记与 rebuild:通过懒标记优化区间更新,rebuild 确保函数有效区间的正确性,平衡更新与查询效率。
- 1
信息
- ID
- 4714
- 时间
- 2000ms
- 内存
- 512MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者