首页 > 其他分享 >【解题报告】「NOI 1995」石子合并

【解题报告】「NOI 1995」石子合并

时间:2024-06-24 13:53:18浏览次数:3  
标签:sz le 1995 NOI rs color int 解题 ls

一个圆形操场上摆放 \(n\ (1\le n\le 100)\) 堆石子。现要将 \(n\) 堆石子合并成一堆,每次只能选相邻的 \(2\) 堆合并成新的一堆,代价为新的一堆的石子数。求将 \(n\) 堆石子合并成一堆的最小代价和、最大代价和。

“最大代价和”的转移证明

\(\color{blue}{\text{结论}}\):对于 \(f_{i,j}\),最优转移一定从 \(f_{i,j-1}\) 或 \(f_{i+1,j}\) 来。下给出证明。

考虑反证,设最优转移从 \(k\ (i<k<j-1)\) 来,记 \(S_1=\sum\limits_{t=i}^{k} a_t\),\(S_2=\sum\limits_{t=k+1}^{j} a_t\)。

我们可以看成 \([i,k]\) 和 \([k+1,j]\) 先分别内部合并,再两者合并。若 \(S_1\ge S_2\),考虑以下情形:先把 \([i,k]\) 内部合并,然后将这一堆与 \(k+1\) 合并,之后进行 \([k+1,j]\) 的内部合并。这样做的好处是,每次有关 \(k+1\) 的合并(除了原本的 \([i,k]\) 和 \([k+1,j]\) 这一次大合并),都能额外得到 \([i,k]\) 堆的贡献,贡献增量 \(\Delta\ge 2S_1-(S_1+S_2)\ge 0\)。\(S_1<S_2\) 对称同理。\(\square\)

四边形不等式优化

若 \(f\) 满足对于 \(\forall a\le b\le c\le d\),有 \(f_{a,c}+f_{b,d}\le f_{b,c}+f_{a,d}\),则称其满足四边形不等式,简记为\(\color{orange}{交叉 \le 包含法则}\)。

考虑 \(f_{i,j}=\min\limits_{i\le k<j}\left(f_{i,k}+f_{k+1,j}+w_{i,j}\right)\) 型区间 dp,先说\(\color{orange}{适用四边形不等式优化的条件}\):\(\color{red}{w 满足四边形不等式}\)。

下给出证明。先证 \(\color{violet}{f\ 满足四边形不等式}\),只需要证 \(f_{i,j}+f_{i+1,j+1}\le f_{i+1,j}+f_{i,j+1}\)(因为这说明 \(f\) 满足凸性)。

按照区间长度从小到大\textbf{归纳证明}。设 \(f_{i+1,j}\) 的决策点为 \(x\),\(f_{i,j+1}\) 的决策点为 \(y\)。若 \(x\le y\),则:

\[\begin{aligned} f_{i,j}+f_{i+1,j+1}&\le \left(\color{olive}{f_{i,x}}+f_{x+1,j}+\color{cyan}{w_{i,j}}\right)+\left(\color{olive}{f_{i+1,y}}+f_{y+1,j+1}+\color{cyan}{w_{i+1,j+1}}\right)\\ &\le \color{olive}{\left(f_{i,y}+f_{i+1,x}\right)}+\left(f_{x+1,j}+f_{y+1,j+1}\right)+\color{cyan}{\left(w_{i,j+1}+w_{i+1,j}\right)}\\ &=f_{i+1,j}+f_{i,j+1} \end{aligned} \]

若 \(x>y\),在 \(f_{i,j}\) 处取决策 \(y\),\(f_{i+1,j+1}\) 处取决策 \(x\) 放缩即可。因此命题成立。

\(\color{orange}{记\ f_{i,j}\ 的决策点为\ s_{i,j}。}\)

再证 \(\color{violet}{f\ 满足决策单调性}\),即证 \(s_{i,j-1}\le s_{i,j}\le s_{i+1,j}\)。对于前者,记 \(x=s_{i,j}\),\(y=s_{i,j-1}\),考虑反证,若 \(x<y\),则:

\[\begin{aligned} && f_{x+1,j-1}+f_{y+1,j}&\le f_{y+1,j-1}+f_{x+1,j}\\ &\iff& \left(\color{teal}{f_{i,x}}+f_{x+1,j-1}+\color{teal}{w_{i,j-1}}\right)+\left(\color{teal}{f_{i,y}}+f_{y+1,j}+\color{teal}{w_{i,j}}\right)&\le \left(\color{teal}{f_{i,y}}+f_{y+1,j-1}+\color{teal}{w_{i,j-1}}\right)+\left(\color{teal}{f_{i,x}}+f_{x+1,j}+\color{teal}{w_{i,j}}\right)\\ &\iff& (f_{i,j-1},x)-(f_{i,j-1},y)&\le (f_{i,j},x)-(f_{i,j},y) \end{aligned} \]

注意到左式 \(\ge 0\),右式 \(\le 0\),因此 \(x<y\) 不优,故 \(s_{i,j-1}\le s_{i,j}\)。后者证明类似。\(\square\)

回到区间 dp,我们在计算 \(f_{i,j}\) 时,只需在 \(k\in [s_{i,j-1},s_{i+1,j}]\) 中遍历即可。

考虑 \(\color{blue}{f\ 每个主对角方向的斜线}\),遍历次数为 \(\sum\limits_{i}(s_i-s_{i-1})=\mathcal O(n)\),因此总时间复杂度为 \(\mathcal O(n^2)\)。

Garsia - Wachs 算法(针对序列的情形)

算法的流程:不妨 \(a_0=a_{n+1}=+\infty\),找到第一个 \(k\) 满足 \(a_{k-1}\le a_{k+1}\),合并 \(a_{k-1}\) 和 \(a_{k}\),并从 \(k\) 往前找到第一个 \(j\) 满足 \(a_j>a_{k-1}+a_k\),将 \((a_{k-1}+a_k)\) 插到 \(j\) 后面。反复执行此流程,直至 \(|a|=1\),代价和为 \(\sum (a_{k-1}+a_k)\)。

算法的证明鸽了,论文找不到。奇偶位分别开一个平衡树维护,\(a_i\) 增量处理,时间复杂度 \(\mathcal O(n\log n)\)。

\(\color{cyan}{坑}\) 记 \(x=a_{k-1}+a_k\),它被插到了 \(a_j\) 后面成为 \(a_{j+1}\),分析可能会带来的新的影响(容易遗漏!):

  • 可能 \(a_{j+1}=a_{j+3}\),要将 \((j+1,j+2)\) 合并;
  • 可能 \(a_{j-1}\le a_{j+1}\),要将 \((j-1,j)\) 合并,\(\color{red}{但这还可能导致\ a_{j-3}\le a_j}\),因此我们需 while 循环直到不能合并为止}。

AC 代码(\(\mathcal O(n\log n)\))

// #include <bits/stdc++.h>
#include <iostream>
#include <cstdio>
#include <cmath>
#include <algorithm>
#include <cassert>
using namespace std;

const int inf = 2e9 + 5;
const int N = 500005;

// mt19937 rng(chrono::steady_clock::now().time_since_epoch().count());
// mt19937 rng(2020);
// 两棵 “递减链状” Treap (奇偶位)
int rnd[N], sz[N], val[N], maxx[N], ls[N], rs[N], tot;
inline int newnode(int x) {
    ++tot, rnd[tot] = rand(), sz[tot] = 1, val[tot] = maxx[tot] = x, ls[tot] = rs[tot] = 0;
    return tot;
}
inline void pushup(int x) {
    sz[x] = sz[ls[x]] + sz[rs[x]] + 1, maxx[x] = max(val[x], max(maxx[ls[x]], maxx[rs[x]])); // !
}
void split_sz(int u, int rk, int &x, int &y) {
    if (!u) {
        x = y = 0;
        return ;
    }
    if (sz[ls[u]] >= rk) {
        split_sz(ls[u], rk, x, ls[u]);
        y = u;
        pushup(u);
    } else {
        split_sz(rs[u], rk - sz[ls[u]] - 1, rs[u], y);
        x = u;
        pushup(u);
    }
}
void split_val(int u, int w, int &x, int &y) {
    if (!u) {
        x = y = 0;
        return ;
    }
    assert(maxx[u] > w);
    if (maxx[rs[u]] > w) {
        // printf(">>> 1\n");
        split_val(rs[u], w, rs[u], y);
        x = u;
        pushup(u);
    } else if (val[u] > w) {
        // printf(">>> 2\n");
        x = u, y = rs[u], rs[u] = 0;
        pushup(u);
    } else {
        assert(maxx[ls[u]] > w);
        split_val(ls[u], w, x, ls[u]);
        y = u;
        pushup(u);
    }
}
int merge(int x, int y) {
    if (!x || !y) return x + y;
    if (rnd[x] < rnd[y]) {
        rs[x] = merge(rs[x], y);
        pushup(x);
        return x;
    } else {
        ls[y] = merge(x, ls[y]);
        pushup(y);
        return y;
    }
}

void dfs(int u) {
    if (!u) return ;
    dfs(ls[u]);
    // printf("%d(%d, %d, ls = %d, rs = %d) ", val[u], u, maxx[u], ls[u], rs[u]);
    // cerr << val[u] << ' ';
    if(val[u]!=inf)printf("%d ", val[u]);
    dfs(rs[u]);    
}

int a[N], n, cur, root[2], ans;

// (可行, 删除数)
pair<bool, int> check(int pos) {
    if (pos <= 3 || pos + 1 > cur) return {false, 0};
    // 找出 a[pos - 1] 和 a[pos + 1]
    int type = !(pos & 1), k1, k2, k3, k4;
    split_sz(root[type], pos / 2 - 1, k1, k2);
    split_sz(k2, 2, k2, k4), split_sz(k2, 1, k2, k3);
    if (val[k2] > val[k3]) {
        root[type] = merge(merge(k1, k2), merge(k3, k4));
        return {false, 0};
    }
    // cerr << "check pos = " << pos << '\n';
    assert(sz[k2] == 1 && sz[k3] == 1);
    int hp = val[k2];
    root[type] = merge(k1, merge(k3, k4));
    
    // 找出 a[pos]
    k1 = k2 = k3 = k4 = 0;
    type ^= 1;
    split_sz(root[type], (pos + 1) / 2 - 1, k1, k2);
    split_sz(k2, 1, k2, k3);
    hp += val[k2];
    root[type] = merge(k1, k3);
    cur -= 2;
    
    ans += hp;
    // cerr << "hp = " << hp << '\n';
    // cerr << "cur = " << cur << '\n';
    // cerr << "odd : "; dfs(root[1]); cerr << '\n';
    // cerr << "even: "; dfs(root[0]); cerr << '\n';
    
    // 剔除 a[cur]
    int tmp1, tmp2, newpos;
    split_sz(root[cur & 1], (cur + 1) / 2 - 1, tmp1, tmp2);
    assert(sz[tmp2] == 1);
    root[cur & 1] = tmp1;
    
    // cerr << "odd : "; dfs(root[1]); cerr << '\n';
    // cerr << "even: "; dfs(root[0]); cerr << '\n';

    k1 = k2 = k3 = k4 = 0;
    split_val(root[0], hp, k1, k2);
    split_val(root[1], hp, k3, k4);
    assert(maxx[k2] <= hp && maxx[k4] <= hp);
    // printf("sz1 = %d, sz2 = %d, (%d, %d), hp = %d\n", sz[k1], sz[k3], sz[k2], sz[k4], hp);
    if (2 * sz[k1] > 2 * sz[k3] - 1) { // 插在 k1 后面
        // cerr << "case 1\n";
        // cerr << sz[k1] << '\n';
        newpos = 2 * sz[k1] + 1;
        root[1] = merge(k3, k4);
        k3 = k4 = 0, split_sz(root[1], sz[k1], k3, k4);
        root[1] = merge(merge(k3, newnode(hp)), k2);
        root[0] = merge(k1, k4);
    } else { // 插在 k3 后面
        newpos = 2 * sz[k3];
        root[0] = merge(k1, k2);
        k1 = k2 = 0, split_sz(root[0], sz[k3] - 1, k1, k2);
        root[0] = merge(merge(k1, newnode(hp)), k4);
        root[1] = merge(k3, k2);
    }

    // 把 a[cur] 加回去
    cur++;
    root[cur & 1] = merge(root[cur & 1], tmp2);

    // cerr << "cur = " << cur << '\n';
    // cerr << "odd : "; dfs(root[1]); cerr << '\n';
    // cerr << "even: "; dfs(root[0]); cerr << '\n';

    int del = 1;
    while (1) {
        pair<bool, int> pref = check(newpos - 1);
        if (!pref.first) break;
        del += pref.second, newpos -= pref.second; 
    }
    del += check(newpos + 1).second;
    return {true, del};
}

void sc() {
    for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
    // for (int i = 1; i <= n; i++) a[i] = rng() % 5 + 1;
    // for (int i = 1; i <= n; i++) printf("%d ", a[i]); puts("");

    a[n + 1] = inf;

    maxx[0] = 0, tot = root[0] = root[1] = ans = 0;
    root[0] = newnode(inf), root[1] = newnode(inf), cur = 2;
    for (int i = 1; i <= n + 1; i++) {
        cur++;
        root[cur & 1] = merge(root[cur & 1], newnode(a[i]));
        // printf("now i = %d, cur = %d\n", i, cur);
        while (check(cur - 1).first) ; // cur 处可能会破坏 “单调递减” 性质, 在 pushup 处特殊处理
        // for (int j = cur - 1; j >= 1; j--) {
        //     assert(!check(cur - 1));
        // }
        // printf("i = %d, cur = %d\n", i, cur);
        // if(1){dfs(root[1]),puts(""),dfs(root[0]),puts("");}
    }
    printf("%d\n", ans);
}

int main() {
    // freopen("../data.in", "r", stdin);
    // freopen("../my.out", "w", stdout);
    while (~scanf("%d", &n) && n) sc();
    return 0;
}

/*
6
4 2 4 2 5 5
5
3 4 1 2 3
25
3 5 2 1 5 2 4 1 3 1 4 1 1 3 3 5 1 5 1 5 4 4 1 1 1 
15
403 500 56 206 54 352 3 232 485 292 473 190 126 465 210 
*/

标签:sz,le,1995,NOI,rs,color,int,解题,ls
From: https://www.cnblogs.com/wlzhouzhuan/p/18264903

相关文章

  • 5034. 【NOI2017模拟3.28】B —— 矩阵树定理和拉格朗日插值的结合
    题目大意给你一棵\(n\)(\(n\le50\))个点的树,可以进行不超过\(k\)次操作,每次断掉一条边,再连上一条边,要求树一直是树,求一共有多少种树的形态。思路把题意转换为对于一个\(n\)个点的完全图,是树边的话权值是\(1\),否则是\(x\)。跑一遍矩阵树定理,矩阵树定理求的是一个图所有生......
  • [题解]P2042 [NOI2005] 维护数列 - Splay解法
    P2042[NOI2005]维护数列一道思路不难,但实现细节很多的平衡树题,调了一天半终于做出来了w。对于初始序列,我们可以直接构建一条链(毕竟一个一个调用插入函数也可能形成一条链)。题解有递归直接构建成一棵严格平衡的二叉树的,这样也可以,常数可能会小一点。其中区间反转就是裸的文艺......
  • P5311 [Ynoi2011] 成都七中
    题目描述给你一棵\(n\)个节点的树,每个节点有一种颜色,有\(m\)次查询操作。查询操作给定参数\(l\r\x\),需输出:将树中编号在\([l,r]\)内的所有节点保留,\(x\)所在连通块中颜色种类数。每次查询操作独立。对于\(100\%\)的数据,所有出现过的数在\([1,10^5]\)之间,保证每......
  • P1199 NOIP2010 普及组 三国游戏
    P1199NOIP2010普及组三国游戏P1199[NOIP2010普及组]三国游戏-洛谷|计算机科学教育新生态(luogu.com.cn)这题虽然是有博弈论的标签,但是完全没必要,直接贪心即可。下面一个武将的最大默契值称为第一默契值,次大为第二,以此类推。如何最大默契值根据题意,通过观察规律,你......
  • [技巧] NOI LINUX 中不用自己手打的Sublime配置环境
    一般我们测试代码时,需要开$O2$,而$Sublime$中的$C++SingleFile$是没有$O2$的,为此,我们需要手打环境;其实是不用的;第一步,打开“文件”中的“其它位置”中的“计算机”;第二步,在上面的搜索栏中搜索“Sublime”;打开图中高亮的,找到如下图中高亮的并打开,复制;发现会......
  • P9999 [Ynoi2000] tmostnrq 题解
    巨大难写题。就这样一个毒瘤的题,还有人把时空缩小成二分之一放在模拟赛,太好笑了。思路首先将询问离线。我们在\(l_i\)处加入这个点,在\(r_i\)处查询这个点在哪里。那么我们就需要有一个数据结构支持让所有树上的节点一起动。考虑所有点往\(x\)处动。那么对于在\(1\si......
  • AtCoder Beginner Contest 359 解题报告
    AtCoderBeginnerContest359吐槽:A-F还算正常,G题你tm给我放了个出过的板子(ABC340G)是几个意思啊???ASimulate.#include<bits/stdc++.h>usingnamespacestd;#definelllonglong#defineendl'\n'#definePBemplace_back#definePPBpop_back#defineMPmake_pai......
  • P1971 [NOI2011] 兔兔与蛋蛋游戏 题解
    Description这些天,兔兔和蛋蛋喜欢上了一种新的棋类游戏。这个游戏是在一个\(n\)行\(m\)列的棋盘上进行的。游戏开始之前,棋盘上有一个格子是空的,其它的格子中都放置了一枚棋子,棋子或者是黑色,或者是白色。每一局游戏总是兔兔先操作,之后双方轮流操作,具体操作为:兔兔每次操作......
  • P1072 [NOIP2009 提高组] Hankson 的趣味题【GCD】
    [NOIP2009提高组]Hankson的趣味题题目描述Hanks博士是BT(Bio-Tech,生物技术)领域的知名专家,他的儿子名叫Hankson。现在,刚刚放学回家的Hankson正在思考一个有趣的问题。今天在课堂上,老师讲解了如何求两个正整数......
  • 《暗时间》第四章 跟波利亚学解题 随笔摘要
    一启发式思维:(联想)过没有桥的河,联想到以前自己走过一颗倒在河上的树而将问题从如何过河转化为如何让树躺再河上启发式思考方法:1.时刻不忘未知量,时刻记住你到底像要求什么,问题是什么2.用特例启发思考3.反过来推导例子:    (1.100根火柴两个人轮流取,每人每次只能取1......