题目链接,参考资料:论文集2018《浅谈保序回归问题》高睿泉
问题的定义如下:
给出一张 DAG ,每个点 有代价 ()。求点值序列 ,满足如果在 中有从 到 的路径(称为 ),则必须有 ,并使得回归代价
最小。
将序列 中 的元素变为 , 的元素变为 ,称为把 向 取整。
点集 的 均值为使得
最小的 。
定义 问题为,在满足 所有限制的同时,还要求 ,求最小回归。
引理 1. 当 时,任意集合 的 均值唯一;当 时,任意集合 的 均值构成一个区间。
引理 2. 任意集合的 均值不小于该集合的最小值,不大于该集合的最大值。
证明:设
则有
容易发现 当 时单调增,当 时也单调不降。而当 时显然有 ,当 时显然有 ,从而得证。
定理 1. 在 问题中,如果:
- 任意 均不在区间 内
- 存在一个最优解序列 的元素也均不在 内
记 是 问题的一个最优解,那么一定存在 的一个最优解 ,使得 向 取整可以得到 。
这个引理并没有它看起来那么自然。
证明:反证法。
假设不存在能取整到 的 最优解,记 是一个元素不在 内且不能取整到 的最优解。
下面我们证明一个引理
引理 3. 。
证明:注意 不在 上有零点,从而正负性恒定。反证法,假设存在 。
考虑 中取到最大值的点集,记为 。显然 不受到来自外部的 限制。如果 ,则显然可以把 全部调整到 。否则会引出矛盾,我们把 调整到剩下的点集中的最小值会使得回归代价更小,从而 并非最优解。
于是必定存在 ,有 或 。
显然这两类点之间没有偏序关系,于是可以分开研究,从而我们接下来可以不失一般性地只证明
不存在这样的 ,使得存在 而且 是最优解。
记这类点为坏点。
设坏点中 取到最大值的那些点构成的点集为 。根据引理 1, 的 均值是一个区间,记为 。显然, 的 函数在 单调降,在 单调增。
(请注意,当我们谈论任何其他集合,这个结论都是不成立的。而且,由于我们说的是 函数单调降,从而如果希望调整 的回归代价就必须使 的所有元素一起调整。)
(请注意,我们甚至不能保证 。其中 是 中元素公用的 值。)
注意由于 已经在坏点中 取到了最大值,而且 ,所以 不受到任何来自外部的 限制。
若 ,则我们增大 中的所有 ,当它们增大到 前(它们的确可以增大)回归代价一直都是单调降的,从而 并非最优解,得出矛盾。
若 ,则我们可以把 中的所有 增大到 (它们的确可以增大),显然这组新的解不比原先的解劣,但是取整成功的点变多了,从而经过有限次操作可以得到一个能取整到 的 最优解,得出矛盾。
若 。此时增大 没有任何益处,我们需要另寻方法得出矛盾。记 为坏点集合。
否则我们设 是在 中 取到最大值的点集。记 的 均值为 。我们不能像上面一样下降 ,因为可能会被 “卡住”。
我们还需要一个引理
引理 4. 对于 问题,必定存在一个最优解 ,使得 。
证明:几乎重复引理 3 的证明过程即可。
从而,采用整体二分的思路:假设当前点集的取值已经确定为 ,则考虑计算 ,即可应用定理 1,把当前点集划分为取值于 或取值于 。
定理 2. 在 问题中,如果:
- 任意点集的 均值均不在 内
- 存在一个最优解序列 的元素也均不在 内
记 是相同 DAG 上的,回归代价为
的 问题的一组最优解,而且取值仅为 。那么一定存在 的一个最优解 ,使得 。
证明:我们接下来证明,这个 问题和 问题等价。
由于任意点集的 均值不在 内,从而结论 1 可以应用到这里:此 问题的最优解取值只能为 。
根据引理 3,存在一组该 问题的最优解取值,只包含 。
接下来只需要验证 ,读者自证不难,从而得到等价。
然后只需要重复定理 1 的证明过程即可。(事实上比定理 1 简单多了)
由于点集 有限,从而 均值和最优解的取值也有限(大不了在一个鬼畜无比的 维空间里大力找这个回归的最值点,而且不会有在某个边界子空间里全是最值的情况,因为边界一定是 的形式,即它们取了某个 均值,运用引理 1 即得到矛盾),我们只需要找一个看起来足够小的 ,考虑 。此时 变为 处回归代价的导数。于是在实数上二分即可。
引理 5. 当偏序关系是一条链,如果有 ,那么最优解必有 。
证明:显然。
从而我们可以单调栈维护各区间的 均值,如果后面的均值比前面大那就去世了,显然合并后这两个区间的取值全会相等,从而重新计算 均值即可。
由于 均值唯一,所以一般比较好求(比如 就是加权平均值),但是 均值是中位数,如果想要 地合并区间需要用可并堆。当然直接启发式合并 也行。
令 ,就变成了一个标准的 问题。应用上面的解法即可。
这里的实现没有对每个区间都维护两个堆,而是只记一个,进入另一个假想的堆的元素直接销毁。相信你仔细想想就能明白这是为什么。
x
using namespace std;
struct leftist{
int c[2];
int dis, val;
int siz;
} H[100005];
void pushup(int x) {
if (H[H[x].c[1]].dis < H[H[x].c[0]].dis) swap(H[x].c[0], H[x].c[1]);
H[x].dis = H[H[x].c[0]].dis + 1;
H[x].siz = H[H[x].c[0]].siz + H[H[x].c[1]].siz + 1;
}
int merge(int x, int y) {
if (!x || !y) return x + y;
if (H[x].val < H[y].val) swap(x, y);
H[x].c[0] = merge(H[x].c[0], y);
pushup(x);
return x;
}
int n;
struct node {
int rt;
int true_siz;
int val;
};
node stk[100005]; int len;
int tgt[100005];
int main() {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
H[i].dis = 1, scanf("%d", &H[i].val), H[i].val -= i, H[i].siz = 1;
for (int i = 1; i <= n; i++) {
stk[++len] = (node){i, 1, H[i].val};
while (len != 1 && stk[len - 1].val > stk[len].val) {
len--; stk[len].rt = merge(stk[len].rt, stk[len + 1].rt);
stk[len].true_siz += stk[len + 1].true_siz;
while (H[stk[len].rt].siz > (stk[len].true_siz + 1) / 2)
stk[len].rt = merge(H[stk[len].rt].c[0], H[stk[len].rt].c[1]);
stk[len].val = H[stk[len].rt].val;
}
}
int cnt = 0, pos = 0;
long long ans = 0;
for (int i = 1; i <= n; i++) {
if (cnt == 0) cnt += stk[++pos].true_siz;
cnt--;
tgt[i] = pos;
ans += abs(H[i].val - stk[pos].val);
}
printf("%lld\n", ans);
for (int i = 1; i <= n; i++) printf("%d ", stk[tgt[i]].val + i);
}
要分数取模的最优化问题,已经完全暴露了啊(
标准的 问题,但是需要支持特殊询问。
由于最优解显然和我们的单调栈扫描顺序无关,从而我们可以处理出一个元素前缀和后缀的单调栈情况。(可以考虑主席树;其实把询问离线,从左到右扫时暴力把右栈回退也可以)
假设修改了的元素是 ,显然不和 在同一个区间的元素和 没有关系,从而直接使用处理好的信息即可。从而问题变为求 所在的区间。
考虑向左合并,其实就是找到最右方的"稳定"左端点 ,如果已知了 它就可以直接二分; 套在外面二分即可。
xxxxxxxxxx
typedef long long ll;
using namespace std;
const int maxn = 100005, p = 998244353;
int norm(int x) { return x >= p ? x - p : x; }
int inv[maxn];
void init() {
inv[1] = 1;
for (int i = 2; i < maxn; i++)
inv[i] = 1LL * (p - p / i) * inv[p % i] % p;
}
struct frac {
ll a;
int l, r;
frac operator + (const frac v) const { return (frac){a + v.a, l, v.r}; }
bool operator < (const frac v) const {
return (__int128)a * (v.r - v.l + 1) < (__int128)v.a * (r - l + 1);
}
} suf[maxn], pre[maxn];
int suf_val[maxn], pre_val[maxn];
int ls, lp;
vector<frac> his_del[maxn];
int n, m;
int A[maxn];
ll S1_[maxn];
int S1[maxn], S2[maxn];
frac newfrac(int l, int r, int del = 0) { return (frac){S1_[r] - S1_[l - 1] + del, l, r}; }
int inline getS1(int l, int r) { return norm(S1[r] - S1[l - 1] + p); }
int inline getavg(int l, int r) { return 1LL * getS1(l, r) * inv[r - l + 1] % p; }
int inline getS2(int l, int r) { return norm(S2[r] - S2[l - 1] + p); }
int inline getVAL(int l, int r) {
int avg = getavg(l, r);
ll ans = getS2(l, r) - 2LL * avg * getS1(l, r) + 1LL * avg * avg % p * (r - l + 1);
return norm((int)(ans % p) + p);
}
struct qry {
int val, id;
}; vector<qry> Qs[maxn];
int ans[maxn];
int get_L(int del, int R) {
int xL = 0, xR = lp;
while (xL < xR) {
int mid = (xL + xR + 1) >> 1;
if (pre[mid] < newfrac(pre[mid].r + 1, R - 1, del)) xL = mid;
else xR = mid - 1;
}
return xL;
}
pair<int, int> get_R(int del) {
int xL = 0, xR = ls;
while (xL < xR) {
int mid = (xL + xR + 1) >> 1;
int L = get_L(del, suf[mid].l);
if (newfrac(pre[L].r + 1, suf[mid].l - 1, del) < suf[mid]) xL = mid;
else xR = mid - 1;
}
return make_pair(get_L(del, suf[xL].l), xL);
}
int main() {
init();
scanf("%d%d", &n, &m);
for (int i = 1; i <= n; i++)
scanf("%d", &A[i]),
S1_[i] = S1_[i - 1] + A[i],
S1[i] = (S1[i - 1] + A[i]) % p,
S2[i] = (S2[i - 1] + 1LL * A[i] * A[i]) % p;
for (int i = 1; i <= m; i++) {
int pos, val;
scanf("%d%d", &pos, &val);
Qs[pos].push_back((qry){val - A[pos], i});
}
suf[0] = (frac){1, n + 1, n};
for (int i = n; i; i--) {
frac now = (frac){A[i], i, i};
while (ls && suf[ls] < now) {
his_del[i].push_back(suf[ls]);
now = now + suf[ls--];
}
suf[++ls] = now;
suf_val[ls] = norm(suf_val[ls - 1] + getVAL(now.l, now.r));
}
ans[0] = suf_val[ls];
for (int i = 1; i <= n; i++) {
ls--;
for (int j = his_del[i].size() - 1; j >= 0; j--) {
suf[++ls] = his_del[i][j];
suf_val[ls] = norm(suf_val[ls - 1] + getVAL(suf[ls].l, suf[ls].r));
}
for (qry e : Qs[i]) {
pair<int, int> qaq = get_R(e.val);
ans[e.id] = norm(pre_val[qaq.first] + suf_val[qaq.second]);
int l = pre[qaq.first].r + 1, r = suf[qaq.second].l - 1;
// printf("%d %d\n", l - 1, r + 1);
int avg = 1LL * norm(getS1(l, r) + e.val) * inv[r - l + 1] % p;
ll tmp = getS2(l, r) - 1LL * A[i] * A[i] + 1LL * (A[i] + e.val) * (A[i] + e.val);
tmp = norm((int)(tmp % p) + p);
tmp -= 2LL * avg * norm(getS1(l, r) + e.val);
tmp += 1LL * avg * avg % p * (r - l + 1);
tmp = norm((int)(tmp % p) + p);
ans[e.id] = norm(ans[e.id] + (int)tmp);
}
frac now = (frac){A[i], i, i};
while (lp && now < pre[lp])
now = pre[lp--] + now;
pre[++lp] = now;
pre_val[lp] = norm(pre_val[lp - 1] + getVAL(now.l, now.r));
}
for (int i = 0; i <= m; i++) printf("%d\n", ans[i]);
}
对于 问题, 可以看成选了 就必须选 ,变为最大权闭合子图问题,是一个经典网络流例题。
众所周知线性基是拟阵。考虑基交换引理:
现有两个基 。对于任意 ,必存在 使得 是一个基。
即,任意两个基都是直接可以通过一次"交换"操作互相到达的。只需要保证:"交换"操作无法使 变小,也无法使 变大。
于是,爆枚举 (respectively,)中的元素和其外的元素,如果能互相替换就有偏序关系。
最后应用上保序回归就做完了!!!
🕊了