2026.8

2026.8.18

qoj8554 Bot Friends

分析一下基本策略:一个球如果向左,左边洞被提前堵上了就贡献 \(1\),没被堵上就停下没有贡献;如果向右,如果右边被提前堵上了就贡献 \(1\),没堵上也停下。

dp。从左往右,发现往左滚不会影响之后的局面,往右滚会有后效性,但除了刚好把右边的洞堵上的球外都是等价的,只关心有多少个球会继续滚,可以到停下的位置计算,所以贡献后置。

\(f_{i,j,0/1}\) 表示前 \(i\) 个球,有 \(j\) 个球往右滚,第 \(i+1\) 个洞是否被堵上的答案。转移时发现,一种情况后面的一个球回来堵上了 \(i+1\) 然后这个球得以往右滚,但是无法保证一定有球来堵。

但思考一下,根本没问题!如果出现 \(i\) 往右滚且 \(i+1\) 空的情况那么后面求一定不会滚到 \(<i\) 的位置,前面一共只有 \(i-1\) 个球 \(i\) 个坑,还会剩下一个,就总共剩下两个,不合法了。所以一定会有球来堵上,具体哪个不用管。复杂度 \(O(n^2)\)

思考过程 & Code (831b)

\(\Rightarrow\) 稍微推了一下就设计出 dp 状态了。

\(\Rightarrow\) 先推了个转移没考虑很后面往回补上的情况,手玩样例发现问题。

\(\Rightarrow\) 差点以为假了,但发现根本不用管是否被堵上。

\(\Rightarrow\) 完成!

#include <bits/stdc++.h>  
using namespace std;  
const int N = 5005;  
const int INF = 1e9;  
int f[N][N], g[N][N];  
char a[N];  
inline void chmax(int &x, int y) {  
    if (x < y) x = y;  
}  
inline void solve() {  
    cin >> a + 1; int n = strlen(a + 1);  
    for (int i = 0; i <= n; i++)  
        for (int j = 0; j <= n; j++)  
            f[i][j] = g[i][j] = -INF;  
    f[0][0] = 0;  
    for (int i = 0; i < n; i++)  
        for (int j = 0; j <= i; j++) {  
            if (a[i + 1] != '>') {  
                chmax(f[i + 1][j], max(f[i][j], g[i][j] + 1));  
                if (j) chmax(g[i + 1][j - 1], max(f[i][j], g[i][j] + 1) + 1);    
            }  
            if (a[i + 1] != '<') {  
                chmax(f[i + 1][j + 1], max(g[i][j], f[i][j]));  
                chmax(g[i + 1][j], max(g[i][j], f[i][j]));  
            }  
        }  
    cout << max(f[n][0], g[n][0]) << '\n';  
}  
int main() {  
    int T; cin >> T;  
    while (T--) solve();  
    return 0;  
}  

qoj8049 Equal Sums

只要见过一次就会做了吧,不知道在哪里见过的。

\(f_{i,j,k}\) 表示前 \(i\)\(a\),前 \(j\)\(b\)\(sum(a)-sum(b)=k\) 的方案数。

首先状态数就是 \(O(n^3V)\) 的,其次,这个转移太蠢了,因为发现要想不重复顺序是先转移第一维再转移第二维,这和朴素的背包暴力有任何区别吗?

想办法将 \(k\) 控制在 \(O(V)\)。最终的目标是相等,所以实际上 \(|k|>V\) 的状态是没用的,因为这一定是某一次又加了多的那一个,只需要没一定加少的那个,这样就可以将 \(k\) 控制在 \([-V,V]\),然后惊喜的发现这个根本不会重复,完美解决,转移前缀和或差分优化下就可以做到复杂度 \(O(n^2V)\)

嗯,水多了加面,面多了加水。

思考过程 & Code (1.1k)

\(\Rightarrow\) 暴力 dp(嗯,甚至都没意识到传统转移会算重,鉴定为错误的过拟合)

\(\Rightarrow\) 想到方向是缩减状态。

\(\Rightarrow\) 对着将值域缩小到 \(O(V)\) 做,水多了加面,面多了加水。

\(\Rightarrow\) 转移的优化就是小菜一碟。

#include <bits/stdc++.h>  
using namespace std;  
const int N = 505;  
const int P = 998244353;  
int la[N], ra[N], lb[N], rb[N], f[N][N][N << 1];  
inline void add(int &x, int y) { (x += y) >= P && (x -= P); }  
int main() {  
    ios::sync_with_stdio(false);  
    cin.tie(0), cout.tie(0);  
    int n, m, V = 0; cin >> n >> m;  
    for (int i = 1; i <= n; i++) {  
        cin >> la[i] >> ra[i];  
        V = max(V, ra[i]);  
    }  
    for (int i = 1; i <= m; i++) {  
        cin >> lb[i] >> rb[i];  
        V = max(V, rb[i]);  
    }  
    f[0][0][V] = 1;  
    for (int i = 0; i <= n; i++)  
        for (int j = 0; j <= m; j++) {  
            if (i || j) {  
                for (int k = 1; k <= 2 * V; k++)  
                    add(f[i][j][k], f[i][j][k - 1]);  
            }  
            for (int k = 0; k < V; k++) {  
                add(f[i + 1][j][k + la[i + 1]], f[i][j][k]);  
                add(f[i + 1][j][k + ra[i + 1] + 1], P - f[i][j][k]);  
            }  
            for (int k = V; k <= 2 * V; k++) {  
                add(f[i][j + 1][k - rb[j + 1]], f[i][j][k]);  
                add(f[i][j + 1][k - lb[j + 1] + 1], P - f[i][j][k]);  
            }  
        }  
    for (int i = 1; i <= n; i++) {  
        for (int j = 1; j <= m; j++)  
            cout << f[i][j][V] << ' ';  
        cout << '\n';  
    }  
    return 0;  
}  

qoj8050 Random Permutation

做题需要关注决策,是继续当前不确定的做法还是转而思考看上去有前途的做法。

随机大胆猜一下,如果不是特别靠近 \(\frac{n}{2}\),那么其作为中位数的区间不会太大。估摸一下,长度为 \(2x\) 的区间 \(i\) 为中位数概率不超过(非常粗略的上界):

\[\frac{\dbinom{i-1}{x}\dbinom{n-i}{x}}{\dbinom{n-1}{2x}}\times 2x \]

带几个数据确实不是很大。最简单的写法是根据 \(i\) 大小来定,找一个关于 \(i,n\) 的分段函数,可能有什么高级函数可以解决,但一次函数关系要么 WA 要么 TLE,于是思考更优秀的乱搞。

上面会出错的一个很大原因是哪怕只有一个不符合既定规律也会错,要找容错率高的方式,也就是尝试找一个对具体情况的剪枝。

比较容易想到的一个方式是,往左右拓展,如果当前和大于某个阈值就声称之后一定不可能合法。对于特别靠近 \(\frac{n}{2}\) 设大点,剩下的可以比较小,成熟写小点调下参可以过。

但是其实当发现正解是乱搞时,不要一股脑在乱搞上撞死,可能其中有些也需要用到正经做法。 比如这题再仔细想下,相当于要找到所有中位数为 \(i\) 的区间的最小右端点和最大左端点,这个实际上可以直接传统 ds 解决,扫描线 + 线段树即可单 \(\log\),完全不怕被卡正确性。

附:如果严格找到正确区间的话,复杂度可以证明期望 \(O(n\sqrt n)\)

思考过程 & Code (881b)

\(\Rightarrow\) 大胆猜测离 \(\frac{n}{2}\) 远区间长度不大。

\(\Rightarrow\) 胡个粗略的数学公式支撑。

\(\to\) 直接开始找最简单一次函数关系尝试通过(交几发意识到没什么前途)

\(\Rightarrow\) 加剪枝,误打误撞找到直接忘左右拓展方法。

\(\Rightarrow\) 调参 + 卡常艰难通过。

(我就不信正解是这个了)

观看复杂度证明

直接传统 ds?

\(\Rightarrow\) 思考确定性找左右端点方法,怎么这么简单。

#include <bits/stdc++.h>  
using namespace std;  
const int N = 6e5 + 5;  
int a[N], cnt[N];  
int main() {  
    ios::sync_with_stdio(false);  
    cin.tie(0), cout.tie(0);  
    int n; cin >> n;  
    for (int i = 1; i <= n; i++) cin >> a[i];  
    long long res = 0;  
    for (int i = 1; i <= n; i++) {  
        int x = a[i], l = 1, r = n;  
        double f = abs(x - n / 2) * 1.0 / (n / 2) ;  
        int up = f > 0.05 ? 150 : 1000;    
        cnt[n] = 1; int ans = 0;  
        for (int j = i - 1, s = 0; j >= 1; j--) {  
            s += a[j] > x ? 1 : -1; cnt[s + n]++;    
            if (abs(s) >= up) { l = j; break; }  
        }  
        for (int j = i, s = 0; j <= n; j++) {  
            if (j ^ i) s += a[j] > x ? 1 : -1;  
            ans += cnt[-s + n] + cnt[-s + 1 + n];  
            if (abs(s) >= up) { r = j; break; }  
        }  
        res += 1ll * ans * x;  
        for (int j = i - 1, s = n; j >= l; j--)  
            s += a[j] > x ? 1 : -1, cnt[s]--;  
    }  
    cout << res << '\n';  
    return 0;  
}  

2026.8.17

qoj8552 Sticks

同理,先刻画怎样的 \(01\) 矩阵是合法的。

如果确定了每个点是由横向还是竖向的木棍覆盖了,那么也是好判断的。

但是如果直接对这个 dp 的话,思考一下是没办法转移的。

进一步观察确定木棍方向后的图,发现合法等价于存在一条从 \((1,1)\)\((n,n)\) 只向下或向右的路径,并且路径左下都被横向木棍覆盖,右上都被纵向木棍覆盖。

于是合法性判断容易用 dp 完成。

转到计数,注意上面的路径不是唯一的会重复算,所以任务就是将方案与路径之间建立双射,即钦定每个方案对应唯一的一条路径记入答案。

尝试一下,将“最容易合法”的那条记入答案。具体来说,在满足左下部分合法的前提下尽可能靠右上。这个带来的额外限制是若往下走后再向右拐,则往下走的最后一步左右必须分别为 01。

预处理一些东西,复杂度 \(O(n^2)\)

思考过程 & Code (1.5k)

\(\Rightarrow\) 想到刻画怎样的 \(01\) 矩阵合法。

\(\to\) 尝试直接对每个点被覆盖情况 dp(不会转移,失败)

\(\Rightarrow\) 进一步观察,发现分界线结构,会判定。

\(\Rightarrow\) 建立双射,会计数。

\(\Rightarrow\) 写代码。调试发现双射的条件又想简单了。

\(\Rightarrow\) 打补丁,完成。

#include <bits/stdc++.h>  
#define int long long  
using namespace std;  
const int N = 3005;  
const int P = 998244353;  
inline void add(int &x, int y) { (x += y) >= P && (x -= P); }  
char a[N][N];  
int f[N][N][2], D[N][N], R[N][N], fd[N][N], fr[N][N], md[N][N], mr[N][N];  
signed main() {  
    ios::sync_with_stdio(false);  
    cin.tie(0), cout.tie(0);  
    int n; cin >> n;  
    for (int i = 1; i <= n; i++)  
        for (int j = 1; j <= n; j++) cin >> a[i][j];  
    for (int i = 1; i <= n; i++)  
        for (int j = 1; j <= n; j++) {  
            D[i][j] = D[i - 1][j], R[i][j] = R[i][j - 1];  
            fd[i][j] = fd[i - 1][j], fr[i][j] = fr[i][j - 1];  
            md[i][j] = md[i - 1][j], mr[i][j] = mr[i][j - 1];  
            if (a[i][j] != '0') {  
                if (D[i][j] == i - 1) {  
                    D[i][j] = i;  
                    if (a[i][j] == '1') md[i][j] = i;  
                } else if (a[i][j] == '1') fd[i][j] = 1;  
                if (R[i][j] == j - 1) {  
                    R[i][j] = j;  
                    if (a[i][j] == '1') mr[i][j] = j;    
                } else if (a[i][j] == '1') fr[i][j] = 1;  
            }  
        }  
    for (int i = 0; i <= n; i++) f[0][i][0] = 1;  
    for (int i = 1; i <= n; i++)  
        for (int j = 1; j <= n; j++) {  
            if (!fr[i - 1][j])  
                f[i][j][1] = f[i - 1][j][1] * (R[i - 1][j] - mr[i - 1][j] + 1) % P;  
            add(f[i][j][1], f[i - 1][j][0]);  
            if (!fd[i][j]) {  
                f[i][j][0] = f[i][j - 1][0] * (D[i][j] - md[i][j] + 1) % P;  
                if (D[i][j] >= i && !fr[i][j - 1])  
                    add(f[i][j][0], f[i][j - 1][1] * (min(R[i][j - 1], j - 2) - mr[i][j - 1] + 1) % P);         
            }                      
        }  
    cout << ((!fr[n][n]) * f[n][n][1] * (R[n][n] - mr[n][n] + 1) + f[n][n][0]) % P << '\n';  
    return 0;  
}  

qoj8553 Exchanging Kubic

第一下看错题了,非空最大子段和,那全问单点不就做完了?然后发现负数会回答 \(0\),所以全部询问一遍可以得到所有正数,问题就变成了确定所有负数。

发现要确定负数需要负数两边都有东西,然后最大子段和跨过负数。

若负数在两端,则值不唯一任取负数;若存在 \(>1\) 的负数段,那么可以只保留最左边的那个取负数剩下的全部取 \(0\),易证不影响答案。

那个现在序列就变成了由长度 \(\ge 1\) 的正数段和长度 \(=1\) 的负数段交替组成。下文以 + 代替正数段,以 - 代替负数段。

手玩,段的绝对值会形成若干由 >< 组成的链 >< 或者 边界 < 或者 > 边界 都可以减少一个负数段。具体地:

  • 若询问 +-+ 答案不为正段和,则可以确定负数是什么。
  • |+| >/< |-| > |+| < |-| </> |+| ,那么如果选了任意一个 - 则中间那段 -+- 一定被选,所以可以直接令右边的那个 - 取中间的 + 的相反数并将右边的 +-+ 合成一个正数段。

PS:上文中所有大于小于号似乎要带个等于号。

每次扫一遍有点蠢了,发现绝对值最小的正数段左右询问一定至少能通过两次询问消掉一个负数段,而负数段数 \(\le \lfloor\frac{n}{2}\rfloor\),所以总次数不超过 \(2n\)

非常良心 \(O(n^2)\) 可以过,就不用写乱七八糟的 set 维护了。

思考过程 & Code (1.9k)

\(\Rightarrow\) 想到先确定正数然后确定负数的思路。

\(\Rightarrow\) 手玩发现确定负数的方法。

\(\Rightarrow\) 被稍微诈骗了一下想到问最小正段附近的次数的对的。

\(\Rightarrow\) 补全对负数在端点和长度 \(>1\) 的判断。

\(\Rightarrow\) 通过。

#include <bits/stdc++.h>  
#define int long long  
using namespace std;  
const int N = 2005;  
const int INF = 1e15;  
int a[N], nxt[N];  
inline int qry(int l, int r) {  
    cout << "? " << l << ' ' << r << '\n';  
    fflush(stdout);  
    int x; cin >> x; return x;  
}  
struct node { int l, r, w; };  
inline void solve() {  
    int n; cin >> n;  
    for (int i = 1; i <= n; i++) a[i] = qry(i, i);  
    for (int i = 1; i <= n; i++) {  
        if (a[i]) continue;  
        int j = i;  
        while (j < n && !a[j + 1]) j++;  
        for (int k = i + 1; k <= j; k++) a[k] = 0;    
        a[i] = -INF, i = j;  
    }  
    for (int i = 1; i <= n; i++) {  
        if (a[i] < 0) {  
            nxt[i] = i; continue;  
        }  
        int j = i, s = a[i];  
        while (j < n && a[j + 1] >= 0) s += a[++j];  
        nxt[i] = j;  
    }  
    int st = 1, ed = n;  
    while (st <= ed && nxt[st] < ed) {  
        if (a[st] == -INF) {  
            st++; continue;  
        } else if (a[ed] == -INF) {  
            ed--; continue;  
        }  
        vector <node> o;  
        for (int i = st; i <= ed; i++) {  
            int s = 0;  
            for (int j = i; j <= nxt[i]; j++) s += a[j];  
            o.push_back({i, nxt[i], s}), i = nxt[i];  
            assert(nxt[i] >= i);  
        }  
        int mn = 0;  
        for (int i = 0; i < o.size(); i += 2)  
            if (o[i].w < o[mn].w) mn = i;  
        if (mn) {  
            node x = o[mn - 2], y = o[mn];  
            int w = qry(x.l, y.r);  
            if (w ^ max(x.w, y.w)) {  
                a[o[mn - 1].l] = w - x.w - y.w, nxt[x.l] = y.r;  
                continue;  
            } else if (mn == o.size() - 1) {  
                a[o[mn - 1].l] = -y.w, ed = x.r;  
                continue;  
            }  
        }  
        if (mn < o.size() - 1) {  
            node x = o[mn], y = o[mn + 2];  
            int w = qry(x.l, y.r);  
            if (w ^ max(x.w, y.w)) {  
                a[o[mn + 1].l] = w - x.w - y.w, nxt[x.l] = y.r;  
                continue;  
            } else if (!mn) {  
                a[o[mn + 1].l] = -x.w, st = y.l;  
                continue;  
            }  
        }  
        a[o[mn + 1].l] = -o[mn].w, nxt[o[mn].l] = o[mn + 2].r;  
    }  
    cout << "! ";  
    for (int i = 1; i <= n; i++) cout << a[i] << ' ';  
    cout << "\n"; fflush(stdout);  
}  
signed main() {  
    int T; cin >> T;  
    while (T--) solve();  
    return 0;  
}  

2026.8.16

qoj8822 Guess The Sequence 2

这属于“套一层”类型的题目,显然,先尝试刻画怎样的区间集合是好的。

\(a_{pos_x}=x\)。最简单的情况是 \(pos_{1\sim x-1}\) 已知然后确定 \(pos_x\),发现此时只需要有区间答案为 \(x\) 即可,因为这个区间只可能包含 \(1\sim x\),而剩下的都确定了。

然后如果有一个未知的话,设其为 \(y\),如果答案为 \(x\) 的区间的交中有 \(y\) 则无法确定,同时此时交换 \(x,y\) 答案不变,这个区间集合不符合条件;否则能确定 \(x\),并且如果任意一个区间包含 \(y\) 的话 \(y\) 也能被确定。

而由这个构造过程可以知道,每个时刻最多留一个未被确定。但是这并不是一个可以直接描述的条件,所以考虑对这个过程来进行 dp。

容易设计状态:\(f_{i,j}\) 表示 \(\le i\) 中只剩下 \(j\) 未被确定,\(j=0\) 代表全被确定。列出转移后发现,瓶颈在于 \(f_{i-1,j}\to f_{i,j}\)\(f_{i-1,j}\to f_{i,0}\) 的转移,因为剩下的转移是 \(O(1)\) 的。

序列随机要派上用场了。设 \(i\) 作为最大值的最大区间为 \(l_i,r_i\)。后者要求答案为 \(i\) 的区间不包含 \(j\) 且所有区间的交包含 \(j\),那么 \(pos_j\in[l_i,r_i]\)。而根据经典结论,\(\sum{r_i-l_i+1}\)\(O(n\log n)\) 的,所以也可以直接转移。

前者看上去比较复杂,但是发现 \(pos_j\notin [l_i,r_i]\) 的转移系数都是一样的可以打个全局 tag,也只用处理 \(pos_j\in [l_i,r_i]\) 的转移,于是也 ok 了。

具体系数随便推下,可能需要带一个求快速幂的 \(\log\),不过也能过。

启示:不会做了先尝试弱化版。

思考过程 & Code (1.6k)

\(\Rightarrow\) 刻画怎样的区间是好的

\(\Rightarrow\) 尝试依次确定每个数在哪个位置。难点在于可能受到更大的数的限制。

\(\to\) 对局面刻画,确定的是 \(x\) 在一段区间(发现弱化条件不好做,放弃)

\(\Rightarrow\) 不会。(不会处理受到更大数限制)

查看题解:怎么无法确定就扔那就是对的?

\(\Rightarrow\) 会前面全确定的情况。

\(\Rightarrow\) 会前面只有一个不确定的情况,然后发现刻画完了。

\(\Rightarrow\) 设计出 dp 方程和转移。

\(\Rightarrow\) 发现某些情况系数相同,联想随机,会了。

\(\Rightarrow\) 第一遍写完发现系数算简单了。

\(\Rightarrow\) 修补一下,完事。

#include <bits/stdc++.h>  
#define int long long  
using namespace std;  
const int N = 5e5 + 5;  
const int P = 998244353;  
inline int qpow(int a, int b = P - 2) {  
    int res = 1;  
    while (b) {  
        if (b & 1) res = res * a % P;  
        a = a * a % P, b >>= 1;  
    }  
    return res;  
}  
int a[N], L[N], R[N], f[N], tg, b[N];  
inline int F(int x) {  
    return !x ? f[0] : f[x] * tg % P;    
}  
inline int cal(int x) {  
    return qpow(2, x) - 1;  
}  
signed main() {  
    ios::sync_with_stdio(false);  
    cin.tie(0), cout.tie(0);  
    int n; cin >> n;  
    for (int i = 1; i <= n; i++)  
        cin >> a[i], b[a[i]] = i;  
    stack <int> q;  
    for (int i = 1; i <= n; i++) {  
        while (q.size() && a[q.top()] < a[i]) q.pop();  
        L[i] = q.size() ? q.top() + 1 : 1, q.push(i);  
    }  
    while (q.size()) q.pop();  
    for (int i = n; i; i--) {  
        while (q.size() && a[q.top()] < a[i]) q.pop();  
        R[i] = q.size() ? q.top() - 1 : n, q.push(i);  
    }  
    f[0] = tg = 1;  
    for (int i = 1; i <= n; i++) {  
        int x = b[i];  
        int w = cal((R[x] - x + 1) * (x - L[x] + 1)) ;  
        int inv = qpow(tg * w % P);  
        f[i] = F(0) * inv % P, f[0] = F(0) * w % P;  
        for (int j = L[x]; j < x; j++) {  
            int y = a[j], o = cal((x - j) * (R[x] - x + 1));  
            f[0] = (F(0) + F(y) * (w - o - cal((j - L[x] + 1) * (R[x] - x + 1))) % P + P) % P;  
            f[y] = F(y) * o % P * inv % P;  
        }  
        for (int j = x + 1; j <= R[x]; j++) {  
            int y = a[j], o = cal((x - L[x] + 1) * (j - x));  
            f[0] = (F(0) + F(y) * (w - o - cal((R[x] - j + 1) * (x - L[x] + 1))) % P + P) % P;  
            f[y] = F(y) * o % P * inv % P;  
        }  
        tg = tg * w % P;  
    }  
    int ans = 0;  
    for (int i = 0; i <= n; i++)  
        ans = (ans + F(i)) % P;    
    cout << ans << '\n';  
    return 0;  
}  

qoj8528 Chords

容易想到区间 dp。设 \(f_{l,r}\) 表示区间 \([l,r]\) 最多能选多少条线段。设右端点为 \(r\) 的线段左端点为 \(L_r\),则有:\(f_{l,r}=\max\{f_{l,r-1},[L_r\ge l](f_{l,L_r-1}+f_{L_r+1,r-1}+1)\}\)

前一项好搞定,后一项有点复杂。但是发现 \(l\) 固定时随 \(r\) 增大答案单调不降,暴力小数据发现变化点数量不多,于是只需要记这些变化点,状态数可以接受。

转移顺序可以对 \(f\) 的值 \(w\) 从小到大转移。问题在于如何快速找到这个变化点。可惜的是这个不能二分因为后者贡献是恰好选了以 \(r\) 为右端点的线段。但是多试一下就会发现换一下固定 \(r\) 维护 \(l\) 变化后的变化点就可以了,因为此时钦定选的线段都相同,具有单调性。

发现此时外层一个二分,内层查 \(f\) 的值一个二分,根本过不去。但是发现 \(t=f_{L_r+1,r-1}\) 是定值相当于查询最大的 \(l\) 使得 \(f_{l,L_r-1}>f_{l,r-1}-t-1\),这个根本不用二分,直接查对应的第几个的变化点,然后外层二分可以换成双指针。还有一个小点是如何获取 \(f_{L_r+1,r-1}\),其实也简单,因为可以直接在 \(w\) 增大的过程中 \(O(1)\) 维护。

最终复杂度应该是 \(O(n\cdot ans)\),能过。据说 \(ans\) 大约是 \(800\)


题解的做法是固定 \(r\) 后直接换维设 \(f_{r,i}\) 表示 \(f_{l,r}=i\) 的最大 \(i\),本质相同,但直接套用 dp 答案与状态换维的 trick 得到。

思考过程 & Code (770b)

\(\Rightarrow\) 想出区间 dp。

\(\Rightarrow\) 暴力发现变化点数量不多,只需要维护变化点。

\(\to\) \(l\) 固定时二分确定变化点。

\(\Rightarrow\) 过不了,发现假了。

\(\to\) 换成双指针,其实错得本质相同。

\(\Rightarrow\) 发现换一下固定 \(r\) 即可。

\(\Rightarrow\) 写完带 \(\log\) 根本过不去。

\(\Rightarrow\) 本来想卡一卡,但观察代码直接精细实现去掉 \(\log\)

#include <bits/stdc++.h>  
using namespace std;  
const int N = 2e5 + 5;  
int L[N], g[N];  
vector <int> f[N];  
int main() {  
    ios::sync_with_stdio(false);  
    cin.tie(0), cout.tie(0);  
    int n; cin >> n;  
    for (int i = 1, x, y; i <= n; i++)  
        cin >> x >> y, L[y] = x;  
    n <<= 1;  
    for (int i = 0; i <= n; i++)  
        f[i].push_back(n);  
    for (int j = 1; j <= n; j++) {  
        bool fl = 0;  
        for (int i = 1, k = 0; i <= n; i++) {  
            if (f[i].size() < j) continue;  
            while (g[i] < f[i - 1].size() - 1 && f[i - 1][g[i] + 1] > L[i]) g[i]++;  
            int t = min(g[i], j - 1);  
            if (L[i] && f[L[i] - 1].size() > j - t - 1)  
                k = max(k, min(L[i], f[L[i] - 1][j - t - 1]));  
            if (k) f[i].push_back(k), fl = 1;  
        }  
        if (!fl) return cout << j - 1 << '\n', 0;  
    }  
    return 0;  
}  
posted @ 2026-08-19 16:49  happy_zero  阅读(8)  评论(0)    收藏  举报