Skip to content

查看源代码:
20251128NOIP.md

---
title: 请输入标题
createTime: 2025/11/28
categories:
    - IT
tags:
    - OI
---

## 教练曰

- 随机生成样例只能检查显然问题。
  - 变量初始化 / 清空
- 一些相近的板子有变式
- 教得太快,忽略经典解法
  - LIS,换变量变成二分,直接维护变成线段树

## 杂

### 没有思路怎么办?

- 手玩样例
- 看数据范围
- 看部分分
- 考虑 DP(无复杂结构无修问题几乎通用)/ 贪心 / 二分 / 图论建模(特别是不明二元关系)……

### 写代码注意什么?

- 数组大小 / 数字范围(`long long`
- 看好每个变量  
  避免相似字母(特别是表示规模的)/输入顺序混淆。
- 想好变量的依赖关系
- 写一个函数之前想好干什么
- 空间(如滚动数组?)  
  \* 空间问题比时间隐蔽,且更严重。  
  另外,“不是有效的 Win32 应用程序”也有可能是爆空间导致的。

### 如何调试?

- 可读地输出函数调用链和关键变量/数组  
  别输出错。
- 拆壳调试不同部分
- 手造极端样例
- 有些输出少的题目可能会用错误的方法部分撞对,在此时也应详尽输出小样例。

## 检查什么?

- 删干净断点,提交前重新编译运行 + `fc` 防止意外修改。
- 检查有没有多的警告。

数组足够大;

## 贪心策略

- 邻项交换 / 逐步调整
- 策略支配
- 化成更简单的贪心之后根据额外条件微调
- 反悔贪心(设计反悔堆)

注:复杂策略时用 `set` 可能比 `priority_queue` 更好。

## DP

### DP 策略

- 设计状态很重要
  - 遇到无法处理的地方,就把它加入状态。

### DP 的优化技巧

- 滚动数组
- 推式子
  - 分离变量
  - 前缀和化
  - 求和化简
    - 求和换序
    - 艾弗森括号
  - 数据结构维护(线段树/单调队列)
- 决策单调性
- 结合贪心

## 顺序无关或单调数列

首先,若顺序无关,可以指定一个顺序。很多贪心是这么运作的。

其次可以桶化。例如统计长度为 $n$,值属于 $[1,m]$ 的严格单调数列,即为 $\displaystyle\binom{n}{m}$。

## 线段树

线段树空间开四倍。

记得在合适位置 `pushup` / `pushdown`

## Tarjan

边双连通分量写法和强连通分量不一样。

:::details 比较(简单的部分不作比较,无向图最好记录边 ID)
```cpp
int dfn[maxn], low[maxn];
int dft;

void tarjan(int u) {
    dft++;
    dfn[u] = low[u] = dft;

    for (int v: G[u]) {
        if (!dfn[v]) {
            tarjan(v);
            low[u] = min(low[u], low[v]);
        } else if (!co[v]) {
        } else {
            low[u] = min(low[u], dfn[v]);
        }
    }

    if (dfn[u]==low[u]) {
    if (fa==-1 || low[u]>dfn[f]){
        ...
    }
}
```
:::

连通分量的求法也有区别,SCC 直接进栈,BCC 需要先标记桥边 ID 再做 flood fill。

有一些东西可以直接在 dfs 树上统计,不用缩点。强连通分量也一样——有些东西在 Tarjan 时可写,就不用二次搜索增加复杂性。

以及,有向图的 Tarjan 序是缩点图反图的一个拓扑序。

### 图论建模

对不明二元关系特攻。可以转成连通性问题。

## KMP

KMP 稍微记一下。不难。

:::details Code
```cpp
int n; char s[maxn];
int m; char t[maxn];
int p[maxn];
vector<int> ans;

void pre() {
    int j = 0;
    for (int i=2; i<=m; i++) {
        while (t[j+1] != t[i]) {
            j = p[j];
        }
        if (t[j+1] == t[i]) {
            j++;
        }

        p[i] = j;
    }
}

void kmp() {
    int j = 0;
    for (int i=1; i<=n; i++) {
        while (t[j+1] != s[i]) {
            j = p[j];
        }
        if (t[j+1] == s[i]) {
            j++;
        }

        if (j == m) {
            ans.push_back(i - m + 1);
            j = p[j];
        }
    }
}
```
:::

## 区间技巧

忽略重复贡献时,考虑给左端点排序,此时可以忽略贡献部分可以表成一个 $(-\infty, \max r]$ 的区间。取每个区间和 $[\max r+1, \infty)$,然后更新 $\max r$。

## 二分

灵活选择 `mid` 的上/下取整。

## 整体二分

一个大概是紫的技巧。也是与移动有关,不过是单边移动,然后同时处理多个询问。

:::details Code
```cpp
int cur;
void shift(int d) {
    if (d > cur) {
        for (int i=cur+1, i<=d; i++) {
            add(cur);
        }
    } else {
        for (int i=d+1; i<=cur; i++) {
            del(cur);
        }
    }
}

void solve(int l, int r, const vector<int> items) {
    int mid = (l + r) / 2;
    shift(mid);

    vector<int> lv, rv;
    for (int i: items) {
        if (checkl(i)) { 
            lv.push_back(i);
        } else {
            rv.push_back(i);
        }
    }

    solve(l, ..., lv);
    solve(..., r, rv);
}
```
:::

`add``del` 是 $T_a$,`check` 是 $T_c$,则时间为 $T_a n \log n + T_c \log n$。

## 其他 trick

曼哈顿/切比雪夫距离互转。

序列往左加往右加等价于拆成两个子列,翻转一个之后拼起来。