模板索引:基本处理方法与思想

VS Code 调整排版快捷键

这里说的“排版 / 缩进”,在 VS Code 里主要对应 格式化(Format)缩进调整

1. 格式化整个文件

这是最常用的“一键重排版”:

  • Windows / LinuxShift + Alt + F
  • macOSShift + Option + F

作用:按照当前语言的格式化规则,统一整理 缩进、换行、空格 等。

2. 格式化选中内容

只对选中的部分进行重排:

  • Windows / LinuxCtrl + K,然后 Ctrl + F
  • macOSCmd + K,然后 Cmd + F

3. 只调整缩进层级

  • 增加缩进Tab
  • 减少缩进Shift + Tab

选中多行后使用会更方便。

打开命令面板:

  • Windows / LinuxCtrl + Shift + P
  • macOSCmd + Shift + P

常用命令

fc比较函数

windows

在windows终端用如下命令得到带行号的差异文件并输出到 \(diff.txt\)

 fc.exe /N 2.out 2.ans > diff.txt

Linux

diff -u 2.out 2.ans > diff.txt

如何把栈开大

windows

这里大小按 \(KB\) 计,\(268435456=256MB\),作用域为当前编译得到的 \(exe\)

g++ C.cpp -o 1 "-Wl,--stack=268435456"

Linux

作用域为当前终端

ulimit -s 262144
# 还支持开到无限大:
ulimit -s unlimited

手写整型快读

inline long long read(){
	char readch=getchar(); ll readtmp=0;
	ll readflag=1;
	while(readch<'0' || '9'<readch){if(readch=='-')readflag=-1;readch=getchar();}
	while('0'<=readch && readch<='9'){readtmp=readtmp*10+readch-'0';readch=getchar();}
	return readtmp*readflag;
}

二分

while (l <= r) {
    int mid = (l + r) / 2;
    if (check(mid)) r = mid - 1;
    else l = mid + 1;
}

倍增

倍增区间长度

有一些题,慢慢倍增长度要优于二分,因为二分每次都从大区间试探,有一些时候小区间容易结合,大区间结合很花时间也不容易出结果。
但是这种步长倍增方法的实际常数在答案子区间不太细碎的情况下常数容易起飞。

点击查看代码
int work(int st){
    int step = 1;
    int r = st;
    while(step > 0){
        int ed = r + step;
        if(ed <= n && check(st, ed)){
            r = ed;
            step <<= 1;
        }
        else step >>= 1;
    }
    return r;
}

离散化

  • 去重
    std::unique(a.begin(), a.end()) 用于将 有序数组 a 去重,返回去重后末尾位置的迭代器,但不会真的缩短容器长度。

  • 只保留互异元素

a.erase(unique(a.begin(), a.end()), a.end());
  • 统计不同元素个数
std::unique(a.begin(), a.end()) - a.begin()

lower_bound() 函数

std::lower_bound(a.begin(), a.end(), x) 用于在有序序列 a 中寻找 第一个大于等于 x 的位置

  • 如果存在,返回该元素的迭代器
  • 如果不存在,返回 a.end()

对于一个 严格递增 的序列,如果 x 在序列中出现,那么 \(x\)排名就是:

std::lower_bound(a.begin(), a.end(), x) - a.begin() + 1

值得注意的是,假设能搜到,那么无论所搜区间调整,如果减的都是 a.begin(),故最终得到的都是其在数组中的正确下标,不用管 0/1-based,即 a[lower_bound(a.begin(), a.end(), x) - a.begin()] = x

位运算

一些约定

  1. 最右边一位是最低位,也就是第 0 位。
  2. 最左边一位是最高位,也就是第 n - 1 位(对 n 位二进制数而言)。
  3. 默认做操作的两个二进制数位数相同。用 x_i 表示二进制数 x 的第 i 位。
  4. 一般不讨论负数的位运算,实际应用中也尽量不要对负数做按位操作。

左移与右移

  • x << y 可以理解为 x × 2^y(在高位不溢出的前提下)。

  • x >> y 可以理解为 \(\left\lfloor \frac{x}{2^y} \right\rfloor\)

  • 应用中常用 1 << i 来表示 \(2^i\)

  • 特别地,当 i > 30 时,建议写成 1ull << i

常见操作

  • 把第 i 位设为 \(1\) : x |= 1 << i;

  • 保证第 i 位原本为 \(1\),再把它改成 \(0\) : x ^= 1 << i;

用二进制表示集合

假设有 0 ~ 31 共 32 个元素,可以把一个二进制串看作一个集合:

  • x_i = 1 表示元素 i 在集合中
  • x_i = 0 表示元素 i 不在集合中

这样就可以用位运算快速完成集合操作:

  • 交集:按位与 &
  • 并集:按位或 |
  • 对称差:按位异或 ^

例如:

  • {1, 2}{2, 3} 的对称差是 {1, 3}

bitset

常用操作

语句 作用
std::bitset<N> s 创建一个长度为 N 的二进制串
s.set(i, 0/1) 把第 i 位设为 0/1,不写第二个参数时默认设为 1
s.test(i) 返回第 i 位的值
s.count() 返回二进制串中 1 的个数
s.reset() 清空为全 0

此外还可以直接使用:

  • &
  • |
  • ^
  • <<
  • >>

按位取反则使用:

s.flip();

说明

声明 std::bitset<N> 时,N 必须是 编译期常量

在 64 位机器上,可以把 bitset 的底层理解为若干个 unsigned long long 组成的数组。

每次位运算时,本质上是按块进行处理,所以时间复杂度一般写作 \(O(n / w)\),其中 w = 64

空间复杂度同样也是 \(O(n / w)\)

二维差分与前缀和

二维差分:区间覆盖,求单点值

[P5542 USACO19FEB] Painting The Barn S

农夫约翰想在谷仓上涂几层油漆,这样在不久的将来就不需要再重新粉刷了。但是,他不想浪费时间涂太多的油漆。结果表明,\(K\) 涂层是最佳用量。请在他画完所有的长方形后,帮他确定谷仓有多少面积被 \(K\) 层油漆覆盖。

输入格式

输入的第一行包含 \(n\)\(K\)
其余 \(n\) 行中的每一行包含四个整数 \(x_1\)\(y_1\)\(x_2\)\(y_2\),描述正在绘制的矩形区域,左下角 \((x_1,y_1)\) 和右上角 \((x_2,y_2)\)。所有 \(x\)\(y\) 值都在 \(0\)\(1000\) 范围内,并且所有矩形都有正面积。

注意:题目给的是点阵。但求的是面积不是点的个数,所以应当取 x1++y1++,从而将点阵图转化为格子图。

int mp[maxn][maxn];

int main(){
    cin >> n >> k;
    for(int i = 1; i <= n; i++){
        int Ax, Ay, Bx, By;
        cin >> Ax >> Ay >> Bx >> By;
        Ax++; Ay++;
        mp[Ax][Ay]++;
        mp[Bx+1][By+1]++;
        mp[Ax][By+1]--;
        mp[Bx+1][Ay]--;
    }

    for(int i = 1; i <= 1000; i++)
    for(int j = 1; j <= 1000; j++){
        mp[i][j] += mp[i-1][j] + mp[i][j-1] - mp[i-1][j-1];
        if(mp[i][j] == k) ans++;
    }
  
    cout << ans << endl;
	return 0;
}

高精度

struct Large {
    vector<int> num; // low -> high, 小的下标是原数的低位
    int len;

    Large(ll x = 0) {
        num.clear();
        if (!x) num.push_back(0);
        while (x) {
            num.push_back(x % 10);
            x /= 10;
        }
        len = num.size();
    }

    inline void trim() { // 清除前导 0
        while (len > 1 && num[len - 1] == 0) len--;
        if (num.size() != len) num.resize(len);
    }

    Large operator=(const Large& b) {
        num = b.num;
        len = b.len;
        return *this;
    } // 赋值操作

    Large operator+(ll x) const {
        return *this + Large(x);
    } // 高精 + 低精

    Large operator+(const Large& b) const {
        Large res;
        res.num.assign(max(len, b.len) + 1, 0);
        res.len = res.num.size();

        for (int i = 0, c = 0; i < res.len; i++) {
            int s = c + (i < len ? num[i] : 0) + (i < b.len ? b.num[i] : 0);
            res.num[i] = s % 10;
            c = s / 10;
        }

        res.trim();
        return res;
    }

    Large operator*(int x) const {
        // 只能写 big = big * x
        // 不能写 big *= x,因为 big *= x 用的是 operator*=
        // 不能写 big = x * big,因为没有重载 operator*(int, Large)

        Large res;
        res.num.assign(len + 10, 0); // 清空后填充
        res.len = len;

        ll c = 0;
        for (int i = 0; i < len; i++) {
            ll v = 1LL * num[i] * x + c;
            res.num[i] = v % 10;
            c = v / 10;
        }

        while (c) {
            res.num[res.len++] = c % 10;
            c /= 10;
        }

        res.num.resize(res.len);
        res.trim();
        return res;
    }

    Large operator*(const Large& b) const {
        Large res;
        res.num.assign(len + b.len + 1, 0);
        res.len = (int)res.num.size();

        for (int i = 0; i < len; i++) {
            for (int j = 0; j < b.len; j++) {
                res.num[i + j] += num[i] * b.num[j];
            }
        }

        for (int i = 0; i + 1 < res.len; i++) {
            res.num[i + 1] += res.num[i] / 10;
            res.num[i] %= 10;
        }

        res.trim();
        return res;
    }

    Large operator/(int x) const {
        Large res;
        res.num.assign(len, 0);
        res.len = len;

        int rem = 0;
        for (int i = len - 1; i >= 0; i--) {
            rem = rem * 10 + num[i];
            res.num[i] = rem / x;
            rem %= x;
        }

        res.trim();
        return res;
    }

    bool operator<(const Large& b) const {
        if (len != b.len) return len < b.len;
        for (int i = len - 1; i >= 0; i--) {
            if (num[i] != b.num[i]) return num[i] < b.num[i];
        }
        return false;
    }

    void print() const {
        for (int i = len - 1; i >= 0; i--) cout << num[i];
        cout << "\n";
    }
};

sort 和 堆 的排序函数第三维写法

sort第三维是实参,只需要一个比较器对象

点击查看代码
sort(vc.begin(), vc.end(), [](const pr& a, const pr& b){
    return a.second < b.second;
});

类似的 sort(a + 1, a + 1 + n, greater<int>()); 要加小括号才是对的,因为不加就是类型,加了才是对象。

堆的第三个位置要求是一个类型

点击查看代码
struct Cmp {
    bool operator()(const pr& x, const pr& y) const {
        return x.se > y.se;
    }
};
priority_queue<pr, vector<pr>, Cmp> qu;

构造输入流 std::istringstream

string s;
getline(cin, s);

istringstream iss(s);

string op;
int x, y;
iss >> op >> x >> y;

值得注意的是,如果先 cin 了东西,再构造,这时 string 往往会读到空串,因为前面的换行还留在缓冲区里。
要吃掉换行。

int n;
cin >> n;
cin.ignore(numeric_limits<streamsize>::max(), '\n');

string s;
getline(cin, s);

cout << s << "\n";

贪心

模型一:最优化

排序/优先队列

模型二:构造

例题1

image

此题分类讨论

  • 对于打完还是赚的的关卡,按照 d 从小到大排序。
  • 否则按照 r 从大到小排序(反正所有的d都得挨,不如先把大的r吃了)
    • 如果允许关卡内的时候血量可以为负,则按照 d,r 差值排序

可以利用交换思想证明(会掉血的关卡先打 r 大的更容易满足后面关卡 d 的条件)
或者正难则反,考虑是把怪复活(颠倒d,r和整个过程)

模型三:反悔贪心

P2949 [USACO09OPEN] Work Scheduling G

农夫约翰有很多工作要做!为了高效地经营农场,他必须从他所做的每一项工作中赚取利润,每项工作只需要一个时间单位。
他的工作日从时间 \(0\) 开始,总共有 \(10^9\) 个时间单位。他目前可以从 \(N\) (\(1 \leq N \leq 10^5\)) 项工作中选择要做的工作,这些工作被方便地编号为 \(1\)\(N\)
虽然理论上他有可能完成所有 \(N\) 项工作,但实际上这是极不可能的,因为他在任何一个时间单位内只能完成一项工作,而截止日期通常会导致他无法完成所有任务。
\(i\) 项工作的截止时间为 \(D_i\) (\(1 \leq D_i \leq 10^9\))。如果他在截止时间前完成第 \(i\) 项工作(如果当前时间为 \(t\),那么仅当 \(D_i > t\) 的时候他能做这个任务,完成后 \(t \rightarrow t+1\)),他将获得 \(P_i\) (\(1 \leq P_i \leq 10^9\)) 的利润。

点击查看代码
#define pr pair<int, int> // 反悔贪心
#define mk make_pair
int n;
pr work[maxn];
priority_queue< int, vector<int>, greater<int> > qu;
int main(){
    ios::sync_with_stdio(0);
    cin.tie(0), cout.tie(0);
    cin >> n;
    for(int i = 1; i <= n; i++)
        cin >> work[i].first >> work[i].second; // 输入分别是截止时间和价值

    sort(work + 1, work + 1 + n); // 按截止时间升序

    ll cnt = 0; // 记录答案
    int num = 0; // 当前完成工作数,也即当前工作耗时
    // cout << "test" << endl;


    for(int i = 1; i <= n; i++){
        if(1 + num > work[i].first){ // 如果做了这个工作时间不够,看看能不能找一个更劣的换
            if(qu.top() < work[i].second){
                cnt -= qu.top();
                qu.pop();
                cnt += work[i].second;
                qu.push(work[i].second);
            }
        }
        else {
            qu.push(work[i].second);
            cnt += work[i].second;
            ++num;
        }
    // cout << "i: " << i << " nt " << cnt << endl;
    }

    cout << cnt << endl;
	return 0;
}

模型四:邻项交换

  • 特点:(构造或最优化)排序的方案。
  • 方法:假设前 i-1 个位置已经确定,判断第 i与 i+1 位交换后能否更优
    邻项交换法的本质:
    将“二维属性”通过消去公共前缀 \(X\) (比如在国王游戏里体现为当前乘积 p),压缩为“一维比较键”(因为两个维度的属性是偏序的),从而在全局建立一个满足传递性的全序。这个全序保证了排序后的序列一定存在最优解。在此基础上,根据题目要求(全做或选子集),配合模拟或反悔堆求出最优值。

思维流程:

  1. 想邻项交换:只看两个,推 X 在前 Y 在后的条件。
  2. 整理出排序键:把条件化简为 f(X) < f(Y)
  3. 按排序键排序后:原题退化为一维顺序扫描问题。
  4. 扫描时若受限于历史最大值:立刻考虑堆(优先队列)做反悔,如果当前事项无法加入,看看是不是比当前堆中最劣的结果更优。

P1080 [NOIP 2012 提高组] 国王游戏

恰逢 H 国国庆,国王邀请 \(n\) 位大臣来玩一个有奖游戏。首先,他让每个大臣在左、右手上面分别写下一个整数,国王自己也在左、右手上各写一个整数。然后,让这 \(n\) 位大臣排成一排,国王站在队伍的最前面。排好队后,所有的大臣都会获得国王奖赏的若干金币,每位大臣获得的金币数分别是:排在该大臣前面的所有人的左手上的数的乘积除以他自己右手上的数,然后向下取整得到的结果。
国王不希望某一个大臣获得特别多的奖赏,所以他想请你帮他重新安排一下队伍的顺序,使得获得奖赏最多的大臣,所获奖赏尽可能的少。注意,国王的位置始终在队伍的最前面。

直接考虑邻项交换策略证明:
假设当前累计乘积为 p.
考虑当前两人 A, B,显然让 A 在 B 前的条件是 \(\max(\frac{p}{A_y}, \frac{p \ * A_x}{B_y}) \le \max(\frac{p}{B_y}, \frac{p \ * B_x}{A_y})\)(向下取整).
将四个式子从左至右编号 \(1-4\),显然 \(1 \le 4, 3 \le 2\).
于是问题转化为;比较 \(2, 4\),即 A 在 B 前的条件是 \(A_x * A_y \le B_x * B_y\)

P5521 [yLOI2019] 梅深不见冬

给定一棵树,根节点为1.
节点 \(i\) 的权值为 \(w_i\)。规定扶苏能在节点 \(u\) 放上梅花当且仅当满足如下条件:

  • 扶苏当前在节点 \(u\)
  • 对于 \(u\) 的所有孩子 \(v\),节点 \(v\) 被放上了 \(w_v\) 朵梅花。

同时,扶苏可以在任意时刻收回任意节点上的梅花,在收回梅花时不需要走到对应节点。
对于每个节点,想在 \(i\) 号节点上放 \(w_i\) 朵梅花,最少要多少朵梅花。

核心问题在于对一个点,放好其每个儿子的顺序,列出式子之后可以发现其实和国王游戏几乎一样

25辽宁省赛M 猫猫虫

猫猫虫喜欢打 CF,但是它很挑剔,它只喜欢打难度不低于它当前能力值的比赛。
形式化的,我们设猫猫虫的能力值为 \(x\)(初始值 \(x=0\))。有 \(n\) 场比赛,第 \(i\) 场比赛的难度是 \(a_i\),隐藏分是 \(b_i\)。当且仅当猫猫虫当前的能力值 \(x \le a_i\) 时,猫猫虫才会参加这场比赛,然后它的能力值将被重置为\(\max(b_i,x).\)
现在你可以任意安排比赛的顺序,请问猫猫虫最多能参加多少场比赛?

第一步:确立“相邻”法则(你的观察)

假设有两场相邻的比赛 \(i(a_i,b_i)\)\(j(a_j,b_j)\),当前能力值为 \(x\)

  • 如果先 \(i\)\(j\) 能成功,那么参加完 \(i\) 后的能力值 \(\max(x,b_i)\) 必须 \(\le a_j\)。因为 \(x\) 一定是小于等于 \(a_i\) 的,所以核心必要条件是: \(b_i \le a_j\)
  • 同理,如果先 \(j\)\(i\) 能成功,核心必要条件就是:\(b_j \le a_i\)

第二步:定义“谁必须在前”(正难则反)

现在,假设 \(j\) 不能排在 \(i\) 前面(即先 \(j\)\(i\) 必死),那么根据上面的式子,必然有:

\[b_j > a_i \tag{1} \]

但是,如果它们俩同时在最优解里被选中,那么它们之间必然存在一个可行的先后顺序。既然 \(j\) 在前不行,那就只能是 \(i\) 在前。所以 \(i\) 在前必须可行,即:

\[b_i \le a_j \tag{2} \]

第三步:把两个不等式“相加”(神来之笔的数学化)

把 (1) 和 (2) 联立,我们得到:

  • 由 (1):\(a_i < b_j\)
  • 由 (2):\(b_i \le a_j\)

把这两个式子左右分别相加(左边的加左边,右边的加右边):\(a_i+b_i<a_j+b_j\)
这个式子告诉我们:如果存在两个比赛,且必须先 \(i\)\(j\),那么必然需要满足这个式子。
反过来想:如果\(a_i+b_i>a_j+b_j\),那么先 \(i\)\(j\) 一定是不可能的(或者两者互不影响,顺序随便),反正把 \(a+b\) 小的那个放前面绝对不亏。

总结:为了不破坏最优解,我们必须保证按照 \(a_i+b_i\) 从小到大排序。因为如果最优解中存在逆序(前面的 \(a+b\) 大于后面的),我们通过上面的推导知道,交换它们让小的到前面,一定不会让原本可行的解变得不可行,即更劣。

由于是要选一个子集,于是我们直接开一个堆,当遇到无法加入的比赛,比较用这个比赛替换掉堆中 \(b\) 最大的比赛能否更优(使得能力值更小)即可保证得到最优比赛序列。

贪心的三个证明思路

方法一:反证法

假设策略不是最优,在此前提下推导出与题设矛盾的结论,说明假设错误,策略最优
比如:P1080国王游戏

方法二:归纳法

已知结论对于 s 正确,且如果对于 x 正确则对 x+1 也正确,就可以推出对于任意不小于 s 的整数都正确。
例:P1803 线段覆盖

方法三:调整法

以任意一个解出发进行调整,直到无论如何调整都不能使答案更优
比如:P1223 排队接水/国王游戏

posted @ 2025-07-09 23:18  [丘李]Chilllee  阅读(46)  评论(1)    收藏  举报