#include <iostream> #include <vector> #include <limits> using namespace std; using ll = long long; // 我们使用 __int128 来防止可能的溢出 // 辅助函数:计算 x / y 的向下取整(y>0),注意 C++ 整数除法对负数是截断 toward 0 ll floorDiv(__int128 x, ll y) { ll res = (long long)(x / y); if ((x % y) != 0 && x < 0) res -= 1; return res; } struct Point { int x; // 位置 ll y; // 对应值(这里用于 R[i] 或 T) }; int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; vector<ll> a(n); for (int i = 0; i < n; i++) { cin >> a[i]; } if (n == 0) return 0; if (n == 1) { cout << 0 << "\n" << a[0] << "\n"; return 0; } // 计算前缀和 A[i] = a[0] + ... + a[i] vector<ll> A(n); A[0] = a[0]; for (int i = 1; i < n; i++) { A[i] = A[i - 1] + a[i]; } ll S = A[n - 1]; // 求最优 b0 = X,使得对每个 i, (i+1)*X + i(i+1)/2 <= A[i] // 即 X <= (A[i] - i(i+1)/2) / (i+1) ll X = numeric_limits<ll>::max(); for (int i = 0; i < n; i++) { ll candidate = floorDiv((__int128)(A[i] - (ll)i * (i + 1) / 2), (i + 1)); if (candidate < X) X = candidate; } // 构造基础等差序列 m[i] = X + i 及其前缀和 M[i] vector<ll> m(n), M(n); for (int i = 0; i < n; i++) { m[i] = X + i; } M[0] = m[0]; for (int i = 1; i < n; i++) { M[i] = M[i - 1] + m[i]; } // 定义 R[i] = A[i] - M[i] vector<ll> R(n); for (int i = 0; i < n; i++) { R[i] = A[i] - M[i]; } // 总补充量 T = S - M[n-1] ll T = S - M[n - 1]; // 下面求满足: // D[0] = 0, D[n-1] = T, 且 D[i] <= R[i] (中间点不超过 R[i]) // 且 D 的差分非负且非递减(即 D 为离散凸函数)的最大 D。 // 根据理论,这个最大整数凸函数可通过“绑定点”确定: // 定义点集 P0=(0,0), P_i=(i, R[i]) (1<=i<=n-2), P_{n-1}=(n-1, T) // 然后取其下凸包,再在各区间内采用“均摊增量”的方式插值。 // 构造点集合 vector<Point> pts(n); pts[0] = {0, 0}; for (int i = 1; i < n - 1; i++) { pts[i] = {i, R[i]}; } pts[n - 1] = {n - 1, T}; // 利用单调栈求下凸包(绑定点序列) // 当新点使得前一段斜率不大时(即新的斜率更低或相等),就弹出前一点。 vector<int> hull; for (int i = 0; i < n; i++) { while (hull.size() >= 2) { int j = hull[hull.size() - 2], k = hull[hull.size() - 1]; // 计算比较:若从 j 到 i 的斜率比从 j 到 k 的斜率更低或相等,则 k 可舍弃 __int128 left = (__int128)(pts[i].y - pts[j].y) * (k - j); __int128 right = (__int128)(pts[k].y - pts[j].y) * (i - j); if (left <= right) { hull.pop_back(); } else { break; } } hull.push_back(i); } // 接下来在每个绑定点区间内均摊增量: // 对于绑定点 p 到 q,设 L = q-p, 总增量 delta = pts[q].y - pts[p].y。 // 最大凸(整数)序列在该区间的增量分布为:令 a = delta / L, r = delta % L, // 则第 k (0<=k<=L)个点的值为: // D[p+k] = pts[p].y + (if k <= L-r then a*k, else a*(L-r) + (a+1)*(k - (L-r)) ). vector<ll> D(n, 0); // 遍历每个区间 for (size_t t = 0; t < hull.size() - 1; t++) { int p = hull[t], q = hull[t + 1]; int L = q - p; // 区间个数(段数) ll delta = pts[q].y - pts[p].y; ll a_val = delta / L; ll r_val = delta % L; // 余数,0 <= r_val < L for (int k = 0; k <= L; k++) { int i = p + k; ll candidate; if (k <= L - r_val) { candidate = pts[p].y + a_val * k; } else { candidate = pts[p].y + a_val * (L - r_val) + (a_val + 1) * (k - (L - r_val)); } // 为保险起见,确保中间点不超过 R[i](但理论上已经满足) if (i > 0 && i < n - 1 && candidate > R[i]) candidate = R[i]; if (i == 0) candidate = 0; if (i == n - 1) candidate = T; D[i] = candidate; } } // 根据 D 求出 d (差分),其中 d[0]=D[0], d[i] = D[i] - D[i-1] (i>=1) vector<ll> d(n, 0); d[0] = D[0]; for (int i = 1; i < n; i++) { d[i] = D[i] - D[i - 1]; } // 最后 b[i] = m[i] + d[i] vector<ll> b(n); for (int i = 0; i < n; i++) { b[i] = m[i] + d[i]; } // 计算最终前缀和 B[i] = sum_{j=0}^{i} b[j],以及操作次数 ops = sum_{i=0}^{n-2} (A[i] - B[i]) vector<ll> B(n); B[0] = b[0]; for (int i = 1; i < n; i++) { B[i] = B[i - 1] + b[i]; } ll ops = 0; for (int i = 0; i < n - 1; i++) { ops += (A[i] - B[i]); } cout << ops << "\n"; for (int i = 0; i < n; i++) { cout << b[i] << (i + 1 == n ? "\n" : " "); } return 0; }
下面给出一个总结,从数学思路到代码实现需要注意的关键点: --- ### 1. 数学模型与问题转化 - **问题描述** 给定数组 a,需要构造一个新序列 b,使得其前缀和满足一定约束(例如 b 的前缀和不超过 a 的前缀和),并且 b 具有“等差”基础(即 b[i] = b₀ + i)加上一个非负的、非递减的“修正项”。同时,还要求操作次数(前缀亏损的总和)最大。 - **分解问题** - **确定基准 b₀**:对任意 i,都有 (i+1)·b₀ + i(i+1)/2 ≤ A[i],因此 b₀ 的上界是 \[ X = \min_{0\le i<n} \left\lfloor\frac{A[i]-\frac{i(i+1)}{2}}{i+1}\right\rfloor. \] 在代码中要特别注意对负数的除法,必须使用数学意义上的向下取整(floor)。 - **构造基准序列**:令 m[i] = X + i,计算其前缀和 M[i],从而定义余量 \[ R[i] = A[i] - M[i], \] 这表示每个前缀还可额外补充多少。 - **构造最大凸函数 D** 目标是求一个离散凸函数 D,使得 - D[0] = 0, D[n-1] = T,其中 T = S – M[n-1],S 为 a 的总和。 - 对任意 i,D[i] ≤ R[i]; - D 的差分 d[i] = D[i] - D[i-1] 非负且非递减。 证明可知:最大的满足这些条件的 D 序列正好对应于在点集合 \[ (0,0), (1,R[1]), \ldots, (n-2,R[n-2]), (n-1,T) \] 上的下凸包(凸包下插值)。 --- ### 2. 凸包构造与分段插值 - **构造下凸包** 使用单调栈对点进行扫描。每当加入一个新点时,比较新的斜率和之前两点的斜率,如果新的斜率更小或相等,就将前一个点舍弃。注意: - 斜率比较时避免浮点数运算,使用交叉乘法(乘积可能非常大时用 __int128)。 - **分段均摊插值** 对于每个凸包区间(绑定点 p 到 q),设: - 区间长度 L = q – p; - 总增量 delta = pts[q].y – pts[p].y。 为了使插值后的 D 在整数上最大且满足凸性,采用“均摊”方法: - 计算整数部分 a = delta / L 和余数 r = delta % L; - 对于区间内每个位置 k (0 ≤ k ≤ L): - 如果 k ≤ L – r,则 D[p+k] = pts[p].y + a * k; - 否则 D[p+k] = pts[p].y + a * (L – r) + (a + 1) * (k - (L - r))。 这样分段插值既保证了整数值又能“均摊”总增量,确保 D 序列尽可能大且依然满足 D[i] ≤ R[i]。 --- ### 3. 代码实现注意点 - **精确的整数运算** - 使用自定义的 `floorDiv` 函数来实现数学意义上的向下取整,特别是处理负数除法时避免 C++ 默认的截断行为。 - 部分计算(如交叉乘法比较斜率)可能涉及大数运算,使用 `__int128` 避免溢出。 - **边界处理** - 计算 b₀ 时要遍历所有前缀,确保取到最小的合法值 X; - 插值时需要特别处理区间两端,保证 D[0]=0 与 D[n-1]=T。 - **凸包构造的细节** - 在构造下凸包时,使用“≤”判断可以避免在斜率相等时保留不必要的中间点,保证后续插值的正确性。 - **恢复最终序列** - 由 D 的差分得到 d 数组,最后 b[i] = m[i] + d[i]; - 根据 b 序列重建前缀和 B,并计算操作次数 ops。 --- ### 总结 1. **数学思路**: 将原问题转化为寻找满足约束的最大离散凸函数 D。利用凸包下插值得到最优 D,并通过均摊增量法在区间内精确插值。 2. **整数精度**: 避免浮点误差,所有计算(特别是涉及除法和斜率比较的部分)都使用整数运算,必要时使用 `__int128` 并自定义 floor 函数处理负数除法。 3. **代码细节**: - 正确计算 b₀ 的最优值 X; - 构造基础等差序列 m,并计算其前缀和; - 定义 R[i] 及 T,构造点集后利用单调栈构造下凸包; - 在每个绑定区间内用均摊法进行整数插值,确保 D 最大; - 恢复最终 b 序列,并计算操作次数。 这样,从数学建模到代码实现,每一步都精心处理了边界、取整和溢出问题,确保最终结果与预期完全一致。