- 代码答疑
【模运算建模,分段线性优化】邻项校准
- @ 2026-7-18 22:06:16
【模运算建模,分段线性优化】邻项校准
题意概括
给定长度为 的序列 ,以及长度为 的序列 。
每次操作可以把某个 增加 。需要让最终序列满足
对所有 都成立,并最小化总操作次数。
由于 ,,不能枚举每个元素最终增加多少,也不能枚举全部 种余数后逐个检查。
样例分析
考虑第一组样例:
假设最终令第一个数模 的余数为 。
由第一个限制可得,第二个数的余数必须满足
所以第二个数的余数为 。
继续根据第二个限制,
所以第三个数的余数为 。
于是最终余数为
原序列只需要分别增加
次,总操作次数为 。
这个过程说明,只要确定第一个数最终的余数,后面所有数的余数都会被唯一确定。
算法思路
整体思路
设第一个数最终模 的余数为 。
根据相邻两项的限制,可以依次推导出所有位置的最终余数。奇数位置的余数形如 ,偶数位置的余数形如 。
对于一个已经确定的目标余数,把 增加到该余数所需要的最少操作次数,也是一个关于 的模函数。
因此,原问题可以转化为:在 中,最小化若干个分段一次函数之和。
函数只会在至多 个关键位置发生跳变。将这些位置排序后,通过二分统计每个候选点两侧的元素数量,就能在 时间内得到答案。
推导过程
确定余数后,单个位置的最小代价
设最终 模 的余数为 。
因为只允许增加 ,所以最少增加次数为
这里的模运算结果取 到 之间的整数。
如果再额外增加 次,最终余数不会改变,但操作次数会更多,因此固定最终余数以后,只需要考虑这个最小非负差值。
用 表示所有最终余数
设
为了满足
必须有
定义 ,并递推
可以归纳得到:
$ R\_i= \begin{cases} (x+C\_i)\bmod M, & i\text{ 为奇数},\\ (-x+C\_i)\bmod M, & i\text{ 为偶数}. \end{cases} $
也就是说,所有奇数位置关于 的系数为 ,所有偶数位置关于 的系数为 。
将每个位置的代价化成统一形式
对于奇数位置 ,定义
这个位置的操作次数为
$ \begin{aligned} (R\_i-A\_i)\bmod M &=(x+C\_i-A\_i)\bmod M\\ &=(x-Z\_i)\bmod M. \end{aligned} $
对于偶数位置 ,定义
这个位置的操作次数为
$ \begin{aligned} (R\_i-A\_i)\bmod M &=(-x+C\_i-A\_i)\bmod M\\ &=(Z\_i-x)\bmod M. \end{aligned} $
因此,奇数位置贡献的函数为
偶数位置贡献的函数为
展开模运算
当 时,奇数位置的贡献可以写成
$ g\_z(x)= \begin{cases} x-z+M, & x\<z,\\ x-z, & x\ge z. \end{cases} $
也可以表示为
其中方括号表示条件成立时取 ,否则取 。
偶数位置的贡献为
$ h\_z(x)= \begin{cases} z-x, & x\le z,\\ z-x+M, & x>z. \end{cases} $
即
设所有奇数位置的 构成多重集合 ,所有偶数位置的 构成多重集合 。
总操作次数为
$ F(x)=\sum\_{z\in O}g\_z(x)+\sum\_{z\in E}h\_z(x). $
展开后得到
$ \begin{aligned} F(x) \={}&(|O|-|E|)x -\sum\_{z\in O}z +\sum\_{z\in E}z\\ &+M\left( |{z\in O\mid z>x}| \+ |{z\in E\mid z\<x}| \right). \end{aligned} $
对于给定的 ,只需要知道:
和
分别将两个集合排序,就可以通过二分查找求出这两个数量。
为什么不需要枚举所有
奇数位置的函数 在 时发生跳变。
在跳变前需要检查 ,跳变后需要检查 。
偶数位置的函数 在 从 变成 时发生跳变,所以需要检查 和 。
因此只需要检查候选集合
$ \begin{aligned} S={}&{0,M-1}\\ &\cup{Z\_i,(Z\_i-1)\bmod M\mid i\text{ 为奇数}}\\ &\cup{Z\_i,(Z\_i+1)\bmod M\mid i\text{ 为偶数}}. \end{aligned} $
候选点数量不超过 。
任意两个相邻跳变位置之间,所有判断条件 和 都保持不变,因此 是关于 的一次函数。
一次函数在一段连续整数区间上的最小值一定出现在区间端点。所有区间端点都已经包含在候选集合中,所以检查这些候选点不会遗漏最优答案。
算法流程
-
读入 、、序列 和序列 。
-
初始化 ,从左到右处理每个位置。
-
对于奇数位置 ,计算
并加入数组 。
-
对于偶数位置 ,计算
并加入数组 。
-
使用
计算下一个位置的常数项。
-
将数组 和 分别排序,并计算它们的元素之和。
-
枚举候选点:
- 和 ;
- 对每个 ,检查 和 ;
- 对每个 ,检查 和 。
-
对每个候选点 :
- 使用
upper_bound求出 中严格大于 的元素数量; - 使用
lower_bound求出 中严格小于 的元素数量; - 根据公式计算 。
- 使用
-
输出所有候选点中最小的 。
正确性说明
引理一:固定最终余数后,算法计算的单点代价最小
设位置 的最终余数为 。
所有能够达到该余数的操作次数均形如
其中 为非负整数。
当 时操作次数最少,因此最小代价为
引理成立。
引理二:固定 后,所有最终余数唯一确定
相邻限制要求
所以已知 后, 的余数唯一确定。
从 开始依次递推,可以唯一得到所有 。
同时,这样得到的余数必然满足每一个相邻限制。
引理成立。
引理三:公式 等于固定 时的最少操作次数
根据引理二,固定 后所有最终余数唯一确定。
对于奇数位置,其最小代价为
对于偶数位置,其最小代价为
根据引理一,各位置分别选择最少增加次数即可,并且不同位置的增加操作互不影响。
因此把所有位置的最小代价相加,恰好得到固定 时的最少总操作次数,即 。
引理成立。
引理四:候选集合中一定包含一个最优解
函数 中,奇数位置的判断条件只会在 附近发生变化,偶数位置的判断条件只会在 附近发生变化。
算法将每个跳变位置及其前一个整数都加入候选集合,同时加入区间边界 和 。
在相邻两个跳变位置之间,所有指示条件保持不变,因此 是一次函数。
一次函数在整数区间上的最小值一定出现在区间端点,而这些端点都被算法检查。
所以至少有一个全局最优的 位于候选集合中。
引理成立。
定理:算法输出满足全部限制所需的最少操作次数
根据引理四,算法枚举的候选集合中包含一个全局最优的 。
根据引理三,算法对每个候选 计算的 ,就是固定该 时能够达到的最少操作次数。
因此算法取所有候选值中的最小值,等于所有 中的最小值,也就是原问题的最优答案。
定理成立。
实现细节与易错点
-
C++ 中负数取模仍可能为负数。计算 和 后,需要在结果小于 时加上 。
-
奇数位置使用
偶数位置使用
两者顺序不能写反。
-
对于奇数位置,需要检查 和 。当 时,前一个位置应当视为 。
-
对于偶数位置,需要检查 和 。当 时,下一个位置应当视为 。
-
奇数集合中需要统计严格大于 的元素数量,因此使用
upper_bound。 -
偶数集合中需要统计严格小于 的元素数量,因此使用
lower_bound。 -
最大答案接近
可能达到约 ,需要使用
long long。 -
候选点重复不会影响正确性,不需要专门去重。
参考实现
#include<bits/stdc++.h>
using namespace std;
using ll = long long;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int n;
ll m;
cin >> n >> m;
vector<ll> a(n), b(n - 1);
for (ll &x : a) cin >> x;
for (ll &x : b) cin >> x;
vector<ll> o, e;
ll c = 0;
for (int i = 0; i < n; i++) {
ll z;
if (i % 2 == 0) {
z = (a[i] - c) % m;
if (z < 0) z += m;
o.push_back(z);
} else {
z = (c - a[i]) % m;
if (z < 0) z += m;
e.push_back(z);
}
if (i + 1 < n) {
c = (b[i] - c) % m;
if (c < 0) c += m;
}
}
sort(o.begin(), o.end());
sort(e.begin(), e.end());
ll so = accumulate(o.begin(), o.end(), 0LL);
ll se = accumulate(e.begin(), e.end(), 0LL);
auto cal = [&](ll x) {
ll co = o.end() - upper_bound(o.begin(), o.end(), x);
ll ce = lower_bound(e.begin(), e.end(), x) - e.begin();
return ((ll)o.size() - (ll)e.size()) * x
- so + se + m * (co + ce);
};
ll ans = min(cal(0), cal(m - 1));
for (ll z : o) {
ans = min(ans, cal(z));
ans = min(ans, cal((z + m - 1) % m));
}
for (ll z : e) {
ans = min(ans, cal(z));
ans = min(ans, cal((z + 1) % m));
}
cout << ans << '\n';
return 0;
}
复杂度分析
递推所有 并构造两个数组需要 时间。
对奇数位置和偶数位置对应的数组排序,需要
时间。
候选点共有 个。每次计算答案需要进行两次二分查找,时间复杂度为 ,因此枚举候选点的总时间复杂度为
总时间复杂度为
两个排序数组共保存 个元素,除此之外只使用常数个变量,因此空间复杂度为
京公网安备11010802045784号
YIZHIYANG 一只羊 LV 9