技巧整理

· · 算法·理论

考试技巧总结

注意事项

详细内容见 警示自己

考试策略

:::info[更固定化的策略 - 推荐程度⭐] 本策略应用于 4 小时、 4 道题的 CSP-S/NOIP 难度考试。

Part1 - 预处理

Part2 - 稳分必得

Part3 - 得分突破

Part4 - 整合检查

:::info[更随机应变的策略 - 推荐程度⭐⭐] 前 15min 读下题,写好模板和对拍。

5min 检查文件名和 freopen()

中间 2h40min 自由发挥,注意:优先性价比高、想周全再写、要争部分分、要构造检验特例、要对拍。 :::

:::info[跟大佬学的策略 - 推荐程度⭐⭐⭐] 做题分四步:想思路,写框架,写代码,调代码。

大佬说,

思维技巧总结

汇总了一些值得记录的题目,供二刷使用。

每一个板块的题目都按照时间排列。

标题是 出处 名称 ,标 Problem 的题目是找不到名字的题。

数据结构

::::info[Problem] 给定 n 个在二维平面上的横纵坐标互不相同的点。

求一个正方形能框住的点构成的集合的个数。

:::info[Hint] 考虑枚举两点然后必选它们。 ::: :::info[sol] **数据结构;二维前缀和;枚举方法。对于有依赖的集合选择问题,考虑钦定必选点或边界点。** 不同的枚举方法决定了代码的编程复杂度和时间复杂度。 先离散化,然后考虑枚举两点 $A,B$ 必选,然后再横向考虑选择一些其他节点。 ![picture](https://cdn.luogu.com.cn/upload/image_hosting/0lz8ywzz.png) 组合计数的不重不漏是由 **横纵坐标互不相同** 保证的。 ::: :::: ::::info[P6225 [eJOI 2019] 异或橙子] 给定 $a_{[1,n]}$ ,单点修改,区间查询“所有字段的异或和”,不强制在线。 $n\le 2\times 10^5,a_i\le 10^9$ 。 :::info[HintA] 异或的性质 $a\oplus 0=a,a\oplus a=0$ 。 对每一个元素单独考虑是否对异或和产生贡献。 ::: :::info[HintB] **数据结构;对于有大量重复的统计、求和问题考虑从不同视角分析。** 发现在区查 $l,r$ 异奇偶时答案为 $0$ ,其余时在区间中且与 $l,r$ 同奇偶的位产生贡献。 ::: :::info[sol] 异或的性质 $a\oplus 0=a,a\oplus a=0$ 。 对每一个元素单独考虑是否对异或和产生贡献。 发现在区查 $l,r$ 异奇偶时答案为 $0$ ,其余时在区间中且与 $l,r$ 同奇偶的位产生贡献。 考虑使用树状数组维护前缀异或。 ```cpp const int N=2e5+5; int n,q,a[N],t[2][N]; void add(int p,int x,int c){ while(x<=n) t[p][x]^=c,x+=-x&x; } int ask(int p,int l,int r){ int ret=0;--l; while(r) ret^=t[p][r],r-=-r&r; while(l) ret^=t[p][l],l-=-l&l; return ret; } signed main(){ n=read(),q=read(); for(int i=1;i<=n;++i) a[i]=read(), add(i&1,i,a[i]); while(q--){ int op=read(),x=read(),y=read(); if(op==1){ add(x&1,x,a[x]); a[x]=y; add(x&1,x,a[x]); } else{ if((x&1)^(y&1)) puts("0"); else write(ask(x&1,x,y)), putchar(10); } } return 0; } ``` ::: :::: ::::info[P1712 [NOI2016] 区间] 给定 $n$ 个区间,选出 $m$ 个区间,使得它们共同包含至少一个位置,求被选中区间的“最长区间长度减去最短区间长度”的最小值。 $n\le 5\times 10^5,m\le 2\times 10^5,~l_i,r_i\le 10^9$ 。 :::info[HintA] 首先离散化,然后考虑按区间长度排序。 ::: :::info[HintB] 发现问题单调性,考虑双指针。 ::: :::info[sol] **数据结构;双指针;对于满足单调性的区间覆盖问题考虑双指针+数据结构。** 首先离散化,然后考虑按区间长度排序。 发现问题单调性,考虑双指针。 准备一颗线段树维护“被覆盖最多的点的覆盖次数”,设该值为 $mx$ 。 双指针维护一个 $mx$ 恰好 $\ge m$ 的区间。 区间滑动过程中找答案即可。 给出代码辅助理解。 ```cpp const int N=1e6+5; int n,m; struct node{ int l,r; bool operator<(const node &p)const{ return r-l<p.r-p.l; } }s[N]; int t[N<<2],g[N<<2]; inline void pushg(int p,int l,int mid,int r){ if(!g[p])return; t[p<<1]+=g[p],t[p<<1|1]+=g[p]; g[p<<1]+=g[p],g[p<<1|1]+=g[p]; g[p]=0; } void add(int p,int l,int r,int fl,int fr,int c){ if(fl<=l and r<=fr){ g[p]+=c,t[p]+=c; return; } int mid=l+r>>1; pushg(p,l,mid,r); if(fl<=mid)add(p<<1,l,mid,fl,fr,c); if(fr>mid)add(p<<1|1,mid+1,r,fl,fr,c); t[p]=max(t[p<<1],t[p<<1|1]); } int ask(int p,int l,int r,int fl,int fr){ if(fl<=l and r<=fr)return t[p]; int mid=l+r>>1,ret=0; pushg(p,l,mid,r); if(fl<=mid)ret=max(ret,ask(p<<1,l,mid,fl,fr)); if(fr>mid)ret=max(ret,ask(p<<1|1,mid+1,r,fl,fr)); return ret; } int a[N<<1],tot,cnt,ans=2e9; unordered_map<int,int>mp; signed main(){ n=read(),m=read(); for(int i=1;i<=n;++i) s[i]={read(),read()}, a[++tot]=s[i].l,a[++tot]=s[i].r; sort(a+1,a+tot+1); for(int i=1;i<=tot;++i) if(i==1 or a[i]!=a[i-1]) mp[a[i]]=++cnt; sort(s+1,s+n+1); int f=1; for(int i=1;i<=n;++i){ add(1,1,cnt,mp[s[i].l],mp[s[i].r],1); if(ask(1,1,cnt,1,cnt)<m)continue; while(ask(1,1,cnt,1,cnt)>=m) add(1,1,cnt,mp[s[f].l],mp[s[f].r],-1),++f; add(1,1,cnt,mp[s[f-1].l],mp[s[f-1].r],1),--f; ans=min(ans,s[i].r-s[i].l-s[f].r+s[f].l); } if(ans==2e9)ans=-1; write(ans); return 0; } ``` ::: :::: ::::info[P2048 [NOI2010] 超级钢琴] 给定长度为 $n$ 的序列 $a$ ,给定 $l,r,m$ 。 求区间长度 $\in [l,r]$ ,且区间和前 $m$ 大的区间。 输出它们的和, $n,m\le 5\times 10^5$ 。 :::info[sol] **数据结构;将区间关系转换为前缀和上点关系。** - 求出第 $m$ 大。 二分第 $m$ 大的值 $v$ 。 - `check:` 求出区间和 $\ge v$ 的合法区间个数。 考虑用数据结构维护 $a$ 的前缀和数组 $s$ 。 扫一遍 $i:1\to n$ ,求 $j\in [i+l-1,i+r-1]$ 的 $s_j\ge s_i+v$ 的个数。 此部分时间复杂度 $O(N\log^2 N)$ 。 - 求出区间和前 $m$ 大的区间和。 扫一遍 $i:1\to n$ ,求 $j\in [i+l-1,i+r-1]$ 的 $s_j\ge s_i+v$ 的个数 $cnt$ 。 求得 $(\sum s_j)-s_i\times cnt$ 。 累加得到答案 $ans$ 并输出。 此部分时间复杂度 $O(N\log N)$ 。 总时间复杂度 $O(N\log^2 N)$ ,卡卡就过了。 ::: :::: ## 搜索 ## 数学 ::::info[P12336 第三心脏] 给定 $a$ 试构造正整数**四元组** $(a,b,c,d)$ 满足: 1. $\sqrt{a^2+b^2+c^2+d^2}=a\oplus b\oplus c\oplus d$。 2. $a<b<c<d<2^{63}$。 无解输出 $-1$,$\oplus$ 是二进制按位异或。 :::info[HintA] 发现 $\sqrt{a^2+b^2+c^2+d^2}>d$。 ::: :::info[HintB] 设原式为 $d+x$。 ::: :::info[HintC] 考虑钦定 $x=1$。 ::: :::info[HintD] 尝试构造一组万能的 $b,c$。 ::: :::info[sol] **数学;构造;解决自由元多的构造问题时考虑钦定元素值或依赖关系。** 发现 $\sqrt{a^2+b^2+c^2+d^2}>d$,设原式为 $d+x$。 则有 $d^2+x^2+2xd=d^2+a^2+b^2+c^2$。 所以 $x^2+2xd=a^2+b^2+c^2$。 又有 $a\oplus b\oplus c=d\oplus (d+x)$。 取 $x=1$,则 $2d+1=a^2+b^2+c^2,~a\oplus b\oplus c=d\oplus (d+1)$。 所以 $d=\frac{a^2+b^2+c^2-1}{2}\in R$。 有因为完全平方数 $\%4=0,1$, 所以 $d$ 是偶数。 所以 $(d+x)\oplus d=1$。 所以只需构造 $a\oplus b\oplus c=1$。 注意到当 $b=2^{62},c=1\oplus a\oplus b$ 时成立。 ```cpp a=read(); if(a==1){ puts("4 28 40"); return 0; } b=1ll<<30; c=1ll^a^b; d=(a*a+b*b+c*c-1)/2; write(b),putchar(' '), write(c),putchar(' '), write(d); ``` ::: :::: ::::info[P4860 Roy&October之取石子II] 有 $n$ 个石子,每次可以取走 $1$ 个或者 $p$ 个($p$ 为质数)。无法操作者失败,判断先手是否必胜。 $n\le 10^{18}$。 :::info[Hint] 由题可知,我们每一次都可以取 $1,2,3$ 个。 ::: :::info[sol] **博弈论;打表找规律;对于难以入手的博弈论问题考虑观察特殊取值,构造必胜策略。** 在本题中,我们发现了 $1,2,3$ 的特殊性,并构造了必胜策略。 由题可知,我们每一次都可以取 $1,2,3$ 个。 显然当 $n$ 被 $4$ 整除时后手有必胜策略。 同理在 $n$ 取余 $4\neq0$ 时先手将其对 $4$ 取余结果变为 $0$ 并成为后手,有必胜策略。 ::: :::: ::::info[P2953 [USACO09OPEN] Cow Digit Game S] 若当前有 $x$ 个石子,设其十进制最大数码为 $a$,最小非零数码为 $b$,玩家可以取走 $a$ 或 $b$ 个石子。最开始有 $n$ 个石子,问先后手谁会获胜? $n\le 10^6$。 :::info[Hint] 注意到 $10^6\times \log (10^6)\le 1.5\times 10^8$。 ::: :::info[sol] **博弈论;对于状态总数较少的博弈论问题考虑暴力计算 SG 。** 注意到 $10^6\times \log (10^6)\le 1.5\times 10^8$。 考虑将每一个状态的 $SG$ 值算出来。 ```cpp for(int i=1;i<=1e6;++i){ int mx=0,mn=10,t=i; while(t){ if(t%10) mx=max(mx,t%10), mn=min(mn,t%10); t/=10; } dp[i]=(!dp[i-mn] or !dp[i-mx]); } while(n--)puts(dp[read()]?"YES":"NO"); ``` ::: :::: ::::info[ARC208A] 有 $n$ 堆石子,第 $i$ 堆有 $a_i$ 个石子。两名玩家依次操作:每次操作可以从某一堆中拿走至少一个石子,需要保证所有 $a_i$ 按位或的结果时刻不变。无法操作者失败,先后手谁会获胜? $n\le 10^6,a_i\le 10^9$。 :::info[HintA] 发现最后整体的状态一定是每一个最初有 $1$ 的位置留下一个。 ::: :::info[HintB] 考虑钦定每一个有 $1$ 的位最后留在哪里,然后剩下的 $1$ 就是需要拿走的。 ::: :::info[HintC] 容易发现,这变成了一个 Nim 博弈。 ::: :::info[sol] **博弈论;Nim 博弈;博弈转换;对于博弈论问题要求保持某特征时观察结束状态;对于类似 Nim 的博弈论问题考虑钦定、转换得到熟悉的问题。** 发现最后整体的状态一定是每一个最初有 $1$ 的位置留下一个。 考虑钦定每一个有 $1$ 的位最后留在哪里,然后剩下的 $1$ 就是需要拿走的。 容易发现,这变成了一个 Nim 博弈。 进一步发现,**对于每一种钦定,对应的 Nim 博弈的必胜者都固定**,则随意选择一种计算即可。 ::: :::: ::::info[Problem] 有 $n$ 堆石子,第 $i$ 堆有 $a_i$ 个石子。两名玩家依次操作:每次操作可以从第一堆中拿走至少一个石子,或者从第 $i$($i>1$)堆中将至少一个石子移动到第 $i?1$ 堆。无法操作者失败,先后手谁会获胜? $n\le 10^7,a_i\le 10^{18}$。 :::info[sol] **博弈论;Nim 博弈;博弈转换;对于类似 Nim 游戏的问题找到不影响答案的状态,转换为 Nim。** 类似于 Nim 博弈中“两个相同数量石子的堆”,本博弈中“编号为偶数的堆”同样不会对结果造成任何影响。 则按照奇数堆部分进行普通 Nim 博弈处理即可。 先手必败当且仅当奇堆异或和为 $0$。 ::: :::: ::::info[P11056 Fire and Big] 有 $m$ 个石子,两人轮流取石子,不能取的人输。\ 给定正整数 $n$,每次取石子的个数 $k$($k$ 是正整数) 必须满足如下两个条件**之一**: - $k$ 是 $n$ 的倍数。 - $k$ 是 $<n$ 的完全平方数。 进行 $T$ 局游戏,每一局 $n$ 不变。 对于每一局,求先手是否存在必胜策略。 $T,n\le 5\times 10^5,m\le 10^9$。 :::info[HintA] 不存在两个 $\%n$ 相同的后手必胜点。 ::: :::info[HintB] 考虑为后手必胜点找上界。 ::: :::info[sol] **博弈论;计算 SG;必胜点上界;对于某限制条件较小 (1e6) 的博弈问题考虑寻找石子数量较大时必胜 / 败的规律,暴力求 SG 。** 不存在两个 $\%n$ 相同的后手必胜点,反证易得。 所以后手必胜点有上界。 所有后手必胜点 $p\le n\times (\sqrt n +1)$。 考虑反证,具体证明过程如下(抄来的)。 > 证明:考虑反证,假设有大于这个数字的后手必胜点,那么考虑两步: > > - 第一步:从后手必胜点走一步到的所有点都必须是先手必胜点,此时对于 $p>n(\sqrt{n}+1)$,有 $>\sqrt{n}$ 种走法是减去 $n$ 的倍数的。 > - 第二步:这 $>\sqrt{n}$ 个和 $p$ 同余的先手必胜点,必须要能走到至少一个后手必胜点。那么再减去 $n$ 的倍数是不可能的(由性质 1),只能减去完全平方数,而减去的方案数只有 $\sqrt{n}$。 注意 **卡空间,要用 bitset 。** ::: :::: ::::info[[P13755 【MX-X17-T4】Yet another Game problem](https://www.luogu.com.cn/problem/P13755)] Alice 和 Bob 又在玩游戏。有一个序列 $a_1,a_2,\ldots,a_n$ 和一个区间 $[l,r]$ 初始为 $[1,n]$。双方都知道所有的信息,Alice 和 Bob 将轮流对这个区间进行操作,Alice 先手。 - 若轮到 Alice 操作,她可以选择一个 $i$($l<i\le r$),并把区间变为 $[i,r]$。 - 若轮到 Bob 操作,他可以选择一个 $i$($l\le i< r$),并把区间变为 $[l,i]$。 当 $l=r$ 时,游戏结束。最终得分即为 $a_l$。 Alice 希望这个最终得分尽可能大,Bob 则希望最终得分尽可能小。假设双方都采用最优策略,请问最终得分会是多少?有时为了防止你蒙混过关,Alice 还要你告诉她第一步应该如何操作。 对于 $100\%$ 的数据,$2\le n\le 10^6$,$\mathit{op} \in\{0,1\}$,$1 \le a_i \le 10^9$。 :::info[Hint] 考虑套上二分转换为 01 串,判定能否拿到 $1$。 ::: :::info[sol] **博弈论;二分;对于求得分、答案为整数的博弈论问题考虑二分,挖掘 check 函数的等价表示。** 考虑套上二分转换为 01 串,判定能否拿到 $1$。 进一步发现判定等价于**能通过一步操作让剩下的后缀 $1$ 的数量大于等于 $0$ 的数量。** 于是就做完了。 ::: :::: ::::info[CF603C] 有 $n$ 堆石子,第 $i$ 堆有 $a_i$ 个。两人轮流操作:每次可以选择一堆减少 $1$ 个石子;或者选择 $a_i$ 为偶数的堆,将其变成 $k$ 堆为 $\frac{a_i}{2}$ 的石子。无法操作者失败,问先手是否存在必胜策略。 :::info[Hint] 注意到每堆石子关系独立。 ::: :::info[sol] **博弈论;对于每堆石子关系独立的博弈论问题,考虑挖掘 SG 函数规律并分别求 SG 。** 注意到每堆石子关系独立。 考虑计算 SG 函数。 $$SG(x)=\begin{cases}\operatorname{mex}{SG(x-1)},&x\bmod 2=1\ \operatorname{mex}\{SG(x-1),SG(\frac{x}{2})\},&x\bmod 2=0\text{ and }k\bmod2=1\\ \operatorname{mex}\{SG(x-1),0\},&x\bmod 2=0\text{ and }k\bmod2=0\end{cases}$$ 观察 SG 的性质即可快速求得。 ::: :::: ::::info[Problem] 给定一棵 $n$ 个节点的树,最开始只有根节点是白色的,其余节点都为黑色。两人轮流进行操作,不可操作者输。 操作指选择一个白色的非叶子节点,将其染黑,并将其至少一个儿子染白(可以不止一个)。 询问先手是否存在必胜策略。 $n\le 10^6$。 :::info[HintA] 考虑在树上求 SG 。 定义 SG (x) 为以 x 构成的子树的游戏的 SG ,目标为 SG (Root) 。 ::: :::info[HintB] 观察 SG 函数的性质。 ::: :::info[sol] **博弈论;树上 SG;挖掘 SG 函数的性质优化转移。** 考虑在树上求 SG 。 定义 SG (x) 为以 x 构成的子树的游戏的 SG ,目标为 SG (Root) 。 显然 SG 函数为儿子集的子集异或和组成的集合的 Mex ,具体来说: $SG(u)=\operatorname{mex}_{T\subseteq S_x}\{\oplus_{v\in T}SG(v)\}

转移时间复杂度太高。

打表/数学归纳法/线性基知识,都可以发现一个关键性质:对于所有 uSG(u) 一定是 0 或者 2 的非负整数次幂。

于是转移优化为 O(n\log n),就做完了。 ::: ::::

::::info[P6599 「EZEC-2」异或] 给定 n,m ,构造 n 个值域 \in[1,m] 的数,使其两两异或之和最大。

输出最大值取模 10^9+7

:::info[HintA] 当 $m=2^k-1$ 时发现每一位二进制互不影响,考虑对每一位单独考虑再累加贡献。 ::: :::info[HintB] 发现对于第 $k$ 位有 $x$ 个数位填 $0$ 时在第 $k$ 位的贡献是 $2^k\times x\times (n-x)$ 。 ::: :::info[HintC] 不难发现 $x=\left \lfloor \frac{n}{2} \right \rfloor$ 时上述式子取到最大值。 ::: :::info[HintD] 简单构造得到,上述方法在 $m\neq 2^k-1$ 时也成立。 ::: :::info[sol] **数学;位运算;对于位运算问题考虑观察每位之间是否影响然后逐位考虑;对于任意问题考虑从特殊情况入手,尝试推广到一般。** 当 $m=2^k-1$ 时发现每一位二进制互不影响,考虑对每一位单独考虑再累加贡献。 发现对于第 $k$ 位有 $x$ 个数位填 $0$ 时在第 $k$ 位的贡献是 $2^k\times x\times (n-x)$ 。 不难发现 $x=\left \lfloor \frac{n}{2} \right \rfloor$ 时上述式子取到最大值。 简单构造得到,上述方法在 $m\neq 2^k-1$ 时也成立。 注意特判和 `long long` 。 给出代码。 ```cpp m=read(),n=read(); if(m==1){puts("0");continue;} int ans=0,s=(n-n/2)%mod*(n/2)%mod; for(int x=1;x<=m;x<<=1ll) ans=(ans+s*x)%mod; write(ans),putchar(10); ``` ::: :::: ## 动态规划 ::::info[P14043 [SDCPC 2019] Flipping Game] 给定 $m,k$ 和一个长度为 $n$ 的只由 $0$ 和 $1$ 组成的字符串 $s$ 表示初始灯的状态,每一次操作可以选择 $m$ 个不同的灯反转状态。现求从初始为全关经过 $k$ 轮后变为指定的 $s$ 状态的方案数,多测。 $n,k\le 100,T\le 100$。 :::info[Hint] 答案只与相对关系有关:发现位置关系不影响答案,同时发现“从初始变成给定状态”可以看成“从给定状态”变成“初始状态”。 ::: :::info[sol] **方案数 DP;对于所有灯的地位都相同的 DP 问题考虑定义 dp[i] 表示有 i 个灯亮的方案数。** 发现求方案数,考虑 DP 。 答案只与相对关系有关:发现位置关系不影响答案,同时发现“从初始变成给定状态”可以看成“从给定状态”变成“初始状态”,定义 $dp_{i,j}$ 表示操作 $i$ 次后存在 $j$ 个不同位置的方案数。 预处理组合数,DP 刷填表皆可,考虑枚举当前有 $f$ 个位置按到已开开关,剩余 $m-f$ 个位置按到未开开关,转移即可。 ```cpp c[0][0]=1; for(int i=1;i<=100;++i){ c[i][0]=1; for(int j=1;j<=i;++j) c[i][j]=(c[i-1][j]+c[i-1][j-1])%mod; } T=read(); while(T--){ n=read(),k=read(),m=read(); s1=READ(),s2=READ(); int cnt=0; for(int i=0;i<n;++i) cnt+=(s1[i]!=s2[i]); for(int i=0;i<=k;++i) for(int j=0;j<=n;++j) dp[i][j]=0; dp[0][cnt]=1; for(int i=0;i<k;++i) for(int j=0;j<=n;++j){ if(!dp[i][j]) continue; for(int f=0;f<=min(j,m);++f){//考虑有f个位置按到已开开关,剩余m-f个位置按到未开开关 //方案数C(j,f) 按完后已开开关还剩j-f+m-f dp[i+1][j+m-2*f]=(dp[i+1][j+m-2*f]+dp[i][j]*c[j][f]%mod*c[n-j][m-f]%mod)%mod; } } write(dp[k][0]),putchar(10); } ``` ::: :::: ::::info[P4310 绝世好题] 给定一个长度为 $n$ 的数列 $a_i$,求 $a_i$ 的子序列 $b_i$ 的最长长度 $k$,满足 $b_i \& b_{i-1} \ne 0 $,其中 $2\leq i\leq k$, $\&$ 表示位运算取与。 $1\leq n\leq 100000$,$a_i\leq 10^9$。 :::info[HintA] 定义 $dp_i$ 为 以 $i$ 结尾的最长子序列长度。 显然有 $dp_i=max\{dp_j+1\}(a_i\&a_j\neq 0)$ 。 暴力转移 $O(N^2)$ ,考虑优化。 ::: :::info[HintB] 发现当 $a_j$ 存在一个二进制位与 $a_i$ 都为 $1$ 时可以转移。 考虑重新定义 DP 含义。 ::: :::info[sol] **动态规划;对于二进制类型的题目可以利用数位不超过 $32$ 重新定义 `DP` 数组。** 定义 $dp_i$ 为 以 $i$ 结尾的最长子序列长度。 显然有 $dp_i=max\{dp_j+1\}(a_i\&a_j\neq 0)$ 。 暴力转移 $O(N^2)$ ,考虑优化。 发现当 $a_j$ 存在一个二进制位与 $a_i$ 都为 $1$ 时可以转移。 考虑重新定义 DP 含义。 定义 $dp_i$ 为 结尾数的二进制第 $i$ 位为 $1$ 的最长序列长度。 则转移时找到所有 $a_i$ 的二进制上为 $1$ 的数位 $j$ 。 则 $dp_j=max\{dp_{\{j\}}+1,dp_j\}$ 。 给出代码。 ```cpp for(int i=1;i<=n;i++){ int x,mx=1;cin>>x; for(int j=0;j<=30;j++) if((1<<j)&x)mx=max(mx,dp[j]+1); for(int j=0;j<=30;j++) if((1<<j)&x)dp[j]=max(dp[j],mx); ans=max(ans,mx); } cout<<ans; ``` ::: :::: :::::info[P10811 【MX-S2-T2】 排] 有 $n$ 个整数 $a_1,a_2,\ldots,a_n$。$f_0=0,f_i= \left\{\begin{aligned}& f_{i-1} & \ f_{i-1}\times a_i>0, \\& f_{i-1}+a_i & \ f_{i-1}\times a_i\le 0.\\ \end{aligned}\right.

重排 a 使得得到的 f_n 最大。

::::info[sol] **背包 DP ;bitset 常数优化 ;挖掘性质与推理。** ### 算法流程 - 找到最大数 - 找到所有“非最大数”凑成的 $<0$ 且最接近 $0$ 的数。 - 如果找不到上述数字或者非最大数能凑成 $0

则输出最大数。

算法正确性证明

综上证毕。

给出代码辅助理解。

const int W=2e6,N=2005;
int n,a[N],mx=0,id,flg;
bitset<2*W+5>dp;
signed main(){
    n=read();
    for(int i=1;i<=n;++i){
        a[i]=read();
        if(a[i]>mx)
            mx=a[i],id=i;
    }
    dp[W]=1;
    for(int i=1;i<=n;++i){
        if(i==id)
            continue;
        if(dp[W-a[i]]){
            /*
            如果最后0能被凑出来,
            这里单独特判的原因是我们初始默认0能被凑出来(f[0]=0)才可以转移。
            */
            flg=1;break;
        }
        //转移 dp[x]|=dp[x-a[i]]
        if(a[i]<0)dp|=dp>>(-a[i]);
        else dp|=dp<<a[i];
    }
    if(!flg)
        for(int i=W-1;i>=1;--i)
            if(dp[i]){
                write(i-W+mx);//如果能凑出负数,则选择合法的最大的负数再给最大数冲上去
                return 0;
            }
    write(mx);//如果凑不出负数则只能选择一个正数计入贡献,肯定选最大的;如果能凑出0则让最大数直接从0冲上去
    return 0;
}

:::: :::::

图论

::::info[P8191 [USACO22FEB] Moo Network G] 有 n 个点二维平面上的点(坐标为整数),两点的距离为欧几里德距离的平方,求它们的最小生成树。

:::info[HintA] 考虑优化 $n^2$ 复杂度的暴力最小生成树算法。 ::: :::info[HintB] 发现 $y\le 10$ ,只考虑可能成为树边的边。 ::: :::info[sol] **图论;优化建边;对于某一个维度非常小的简单算法问题考虑删去无用边优化建图;人类智慧。** 考虑优化 $n^2$ 复杂度的暴力最小生成树算法。 发现 $y\le 10$ ,只考虑可能成为树边的边。 注意到:每个点 $(x_i,y_i)$ 对于一个 $y$ 都只可能与“ $x_j<x_i$ 且 $x_j$ 最大的 $j$ 点”或者“ $x_j>x_i$ 且 $x_j$ 最小的 $j$ 点”连边。如下图中的绿色点可以与 $i$ 连边,黄色点与 $i$ 连边一定不优秀。 ![](https://cdn.luogu.com.cn/upload/image_hosting/b35fvc66.png) 考虑证明,设与某黄色点 $k$ 连了边。 能找到绿色点 $j$ 使得 $x_j,x_k$ 在同时 $>x_i$ 或者同时 $<x_i$ 。 - 情况 $1$ :若 $i$ 与 $j$ 有连边,如下图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/qmd65yu2.png) 则连边 $i\to j,~j\to k$ 一定更短且合法,如下图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/o4wb3ddz.png) - 情况 $2$ :若 $i$ 与 $j$ 无连边,如下图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/fxe2s8jo.png) 则根据勾股定理,将连边 $i\to k$ 改为 $i\to j$ 一定更优,如下图所示。 ![](https://cdn.luogu.com.cn/upload/image_hosting/gzfpya1b.png) 即证毕。 ```cpp const int N=1e5+5; struct node1{ int x,y; bool operator<(const node1&p)const{ if(x==p.x)return y<p.y; return x<p.x; } }a[N]; int dit(int i,int j){ return (a[i].x-a[j].x)*(a[i].x-a[j].x)+(a[i].y-a[j].y)*(a[i].y-a[j].y); } struct node2{ int x,y,v; bool operator<(const node2 &p)const{ return v<p.v; } }s[N<<5]; int n,ans,tot,fa[N]; int ask(int x){ if(x==fa[x])return x; return fa[x]=ask(fa[x]); } int t[11]; signed main(){ n=read(); for(int i=1;i<=n;++i) a[i]={read(),read()},fa[i]=i; sort(a+1,a+n+1); for(int i=1;i<=n;++i){ for(int j=0;j<=10;++j) if(t[j]) s[++tot]={i,t[j],dit(i,t[j])}; t[a[i].y]=i; } for(int i=0;i<=10;++i) t[i]=0; for(int i=n;i>=1;--i){ for(int j=0;j<=10;++j) if(t[j]) s[++tot]={i,t[j],dit(i,t[j])}; t[a[i].y]=i; } sort(s+1,s+tot+1); for(int i=1;i<=tot;++i){ int x=s[i].x,y=s[i].y; int fx=ask(x),fy=ask(y); if(fx==fy)continue; ans+=s[i].v; fa[fx]=fy; } write(ans); return 0; } ``` ::: :::warning[**我们充分发扬人类智慧**] 我们充分发扬人类智慧: 因为 $y$ 值很小,所以对距离的影响也很小。 直接按照 $x$ 坐标排序,$x$ 坐标相同按 $y$ 排,每个点连它前面的 $25$ 个点跑 `Kruskal` 。 这样速度快的飞起,最慢的点只跑了不到 `500ms` 。 :::: ::::info[P4826 [USACO15FEB] Superbull S] 给定一个长为 $n$ 的序列 $a$, 每次选择 $a_i,a_j,i\neq j$,获得 $a_i\oplus a_j$ 的得分并删除 $a_i,a_j$ 中的一个。 进行 $n-1$ 轮后游戏结束,求最大得分和。 $n\le 2000,a_i\le 10^9$。 :::info[Hint] 注意到每一步最多只有 $n^2=4\times 10^6$ 种得分。 ::: :::info[sol] **图论;建模转换;生成树;对于单次得分状态不多的集合合并问题考虑建图转换为图论问题。** 注意到每一步最多只有 $n^2=4\times 10^6$ 种得分。 考虑把得分关系转换成边,求图的最大生成树。 ::: :::: ## 贪心 ::::info[P7522 ⎌ Nurture ⎌] 有一个长度为 $n$ 的序列 $a$,每次取出两个数 $x,y$,然后插入 $x-y$ 或者 $y-x$。 求最后剩下的数的最大值。 :::info[Hint] 考虑转换问题。 ::: :::info[sol] **贪心;问题转换;对于集合合并累加贡献问题考虑贪心。** 特判 $n=1$ 后,原问题等价于“给定长度为 $n$ 的序列 $a$,把每一个数前都加上正负号,相加后的最大值,要求至少一个正和负”。 显而易见的,我们让序列中最大的数是正号,让序列中最小的数是负号,其余的数取绝对值加入答案贡献。 ::: :::: ::::info[P8093 [USACO22JAN] Searching for Soulmates S] 给定正整数 $a,b$ 。 定义操作为:$a*=2$ 或者 在 $a$ 为偶数时 $a/=2$ 或者 $a+=1$ 。 求 $a$ 变成 $b$ 的最小操作次数,多测。 $T\le 10,~a,b\le 10^{18}$ 。 :::info[HintA] 注意到操作次数最高可超过 $60$ ,放弃双向搜索,考虑贪心。 ::: :::info[HintB] **性质:** 如果当前 $a>b$ ,则最优策略为:将 $a$ 变为偶数(如果已经是就忽略),然后将 $a\div 2$ 。 ::: :::info[HintC] **性质:** $a$ 一旦开始 $\times 2$ 后就不会 $\div 2$ 。 ::: :::info[sol] **贪心;挖掘性质;相对关系:对 $a\times 2$ 等价于对 $b\div 2$。** 注意到操作次数最高可超过 $60$ ,放弃双向搜索,考虑贪心。 **性质:** 如果当前 $a>b$ ,则最优策略为:将 $a$ 变为偶数(如果已经是就忽略),然后将 $a\div 2$ 。 **证明:** 在 $a>b$ 时如果有 $\times$ ,则最后一定只能除下来。 我们分析一下最后一次 $\times$ : - 如果最后一次 $\times$ 是乘上去后加了一些东西再除下来的 那么你在不乘的时候加效果是一样的,而且乘后加需要两倍的操作次数,一定不优。 - 如果最后一次 $\times$ 是乘上去后没加东西又除下来了 那瞎折腾白浪费操作次数,一定不优。 所以没有最后一次 $\times$ ,归纳即得 $a>b$ 时不可能出现乘法。 所以只能 $+,\div$ 。 而 $+$ 两次及以上 不如 先除再加效率翻倍,所以只有“先变偶再 $\div 2$”的策略才最优。 得证。 **性质:** $a$ 一旦开始 $\times 2$ 后就不会 $\div 2$ 。 **证明:** 考虑反证,如果存在 $a\times 2$ 后 $\div 2$ 。 则可以找到一段操作为 $\times 2,+k,\div 2$ 。 为保证 $\div$ 合法性, $k$ 应为偶数。 显然上述操作等价于 $+\frac{k}{2}$ ,而这一定更优。 于是不存在一段操作为 $\times 2,+k,\div 2$ 。 于是一旦开始 $\times 2$ 后就不会 $\div 2$ 。 证毕。 **性质:** $a$ 一旦开始 $\div 2$ 之后就不会 $\times 2$ 。 与上述证明同理,故略。 **性质:** $a$ 一定是先进行 $+,\div$ 再进行 $+,\times$ ;除最后一次 $\div$ 操作外,$+$ 不会出现连续的 $2$ 次;最后一次操作不会出现连续 $4$ 次 $+1$ 。 综合上述两性质易证第一句话。 对于第二句话,如果在当前存在连续两次 $+1$ ,则在上一步操作时 $+1$ 一次。 效果相同,操作次数更少,于是得证。 对于第三句话,如果在某状态连续三次 $+1$ ,则一定可以构造一组更优方案。 综上证毕。 **做法:** 考虑不断将 $a$ 变偶再 $\div 2$ 。 每得到一个数,考虑从这个数出发利用 $\times 2,+1$ 操作变为 $b$ 。 在此过程中统计最优答案。 **代码:** ```cpp int T,n,m,ans; signed main(){ T=read(); while(T--){ n=read(),m=read(),ans=1e9;int cnt=0; while(n>m){ if(n&1)++n,++cnt; n/=2,++cnt; } while(n){ int tmp=m,tot=0; while(tmp/2>=n){ if(tmp&1)--tmp,++tot; tmp/=2,++tot; } ans=min(ans,abs(n-tmp)+cnt+tot); if(n&1)++n,++cnt; n/=2,++cnt; if(n==1)break; } write(ans);putchar(10); } return 0; } ``` ::: :::: ::::info[P7384 「EZEC-6」分组] 给你 $n$ 个数,将它们分成任意组(可以不连续),将每个组内的数按位或,并将各组的结果累加,在保证其结果最小的情况下求最多分成的组数。 $n\le 10^7,a_i\le 10^{18}$ 。 :::info[HintA] 发现 $a+b\ge (a\mid b)$ ,当且仅当 $a\& b=0$ 时取等。 于是二进制某一位上都为 $1$ 的数一定在同一组内。 ::: :::info[HintB] 可以把每一个数和 $61$ 个二进制位看成一个图,数 $a_i$ 和位 $j$ 有连边意味着 $a_i$ 的第 $j$ 位是 $1$ 。 题意为求连通块个数。 建图做或用并查集,时间复杂度 $O(n\times \log a)$ ,介于时限 $400ms$ ,无法通过,考虑优化。 ::: :::info[sol] **位运算;贪心。** 发现 $a+b\ge (a\mid b)$ ,当且仅当 $a\& b=0$ 时取等。 于是二进制某一位上都为 $1$ 的数一定在同一组内。 可以把每一个数和 $61$ 个二进制位看成一个图,数 $a_i$ 和位 $j$ 有连边意味着 $a_i$ 的第 $j$ 位是 $1$ 。 题意为求连通块个数。 建图做或用并查集,时间复杂度 $O(n\times \log a)$ ,介于时限 $400ms$ ,无法通过,考虑优化。 **考虑让数 $x$ 加入 $lowbit_x$ 的组,在最后将组合并。** 具体来说,加入 $x$ 时, $s_{\log (-x)\&x}\mid =x$ 。 合并时,对于 $s_i$ ,检查是否存在 $s_i\&s_j\neq 0~(j\neq i)$ 。 若存在,则将 $s_j\mid =s_i$ 并删除 $s_i$ ,相当于并查集处理中 $i,j$ 两个二进制位及其所在联通块合并了。否则找到一个连通块,答案 $+1$ 。 需要注意,因为 $2^0=1>0$ ,所以 $0$ 需要提前特殊判断。 需要开 `long long` 。 放上代码辅助理解。 ```cpp int n,s[62],ans; signed main(){ read(n); for(int i=1;i<=n;++i){ int x;read(x); if(!x){ ++ans; continue; } s[signed(log2(-x&x))]|=x; } for(int i=0;i<60;++i){ for(int j=i+1;j<60;++j) if(s[i]&s[j]){ s[j]|=s[i],s[i]=0; break; } if(s[i])++ans; } write(ans); return 0; } ``` ::: :::: ::::info[P12880 [蓝桥杯 2025 国 C] 数字配对] 给定长度为 $n$ 的序列 $a$ ,取出若干数对 $a_i,a_j$ 使得 $i<j,~a_j=a_i+1$ ,每一个数至多被取 $1$ 次,求最多取多少对。 $n\le 10^6, a_i\le 10^6$ 。 :::info[HintA] **性质:** 如果当前最小值 $a_i=a_j=x,a_k=x+1,i<j<k$ ,则 $(j,k)$ 一定比 $(i,k)$ 更优。 ::: :::info[HintB] **性质:** 如果当前最小值中下标最大的点为 $i$ ,且 $a_i=x,a_j=a_k=x+1,i<j<k$ ,则 $(i,k)$ 一定比 $(i,j)$ 更优。 ::: :::info[HintC] 考虑记录每一种数值的出现下标位置,按照数值从小到大贪心,在过程中用 **双指针** 维护信息。 ::: :::info[sol] **贪心;双指针;对于配对问题考虑从数值角度贪心。** **性质:** 如果当前最小值 $a_i=a_j=x,a_k=x+1,i<j<k$ ,则 $(j,k)$ 一定比 $(i,k)$ 更优。 **证明:** 显然 $i,j$ 都只能与 $x+1$ 的位置组对。 $(i,k)$ 与 $(j,k)$ 对答案的贡献同为 $1$ ,但是 $i<j$ ,所以“能与 $j$ 成对的 $x+1$ 的位置”都能与 $i$ 成对。于是匹配 $(j,k)$ ,留下 $i$ 一定更优。 证毕。 **性质:** 如果当前最小值中下标最大的点为 $i$ ,且 $a_i=x,a_j=a_k=x+1,i<j<k$ ,则 $(i,k)$ 一定比 $(i,j)$ 更优。 **证明:** 显然如果 $j,k$ 与值为 $x$ 的点都能成对,而 $j$ 比 $k$ 更能和值为 $x+2$ 的点成对,于是匹配 $(i,k)$ ,保留 $j$ ,一定更优。 证毕。 **做法:** 综合上面的性质,算法应该很显然了。 考虑记录每一种数值的出现下标位置,按照数值从小到大贪心。 对于数值 $i+1$ 的下标最大的点(下标设为 $x$ ),不断找到数值 $i$ 中最后下标一个 $\ge x$ 的位置进行匹配。 **代码:** ```cpp const int N=1e6+5; int n,ans; vector<int>v[N]; signed main(){ n=read(); for(int i=1;i<=n;++i){ int x=read(); v[x].push_back(i); } for(int i=1;i<=1e6;++i){ while(v[i+1].size()){ while(v[i].size() and v[i].back()>=v[i+1].back()) v[i].pop_back(); if(!v[i].size())break; ++ans,v[i+1].pop_back(),v[i].pop_back(); } } write(ans); return 0; } ``` ::: :::: ::::info[P2107 小 Z 的 AK 计划] 给定长度为 $n$ 的序列 $a,b$ ,有集合 $A=\{i\mid i\in [1,n]\}$ ,对于一个子集 $B\in A$ ,其代价为 $max_{i=1}^{n}a_{B_i}+\sum_{i=1}^nb_{B_i}$ ,求出代价 $\le m$ 时 $|B|$ 最大值。 $n\le 10^5,a_i,b_i\le 10^9$ 。 :::info[sol] **反悔贪心;形式化问题。** ### 独特的视角 考虑枚举 $B_i=x$ 作为 $max_{i=1}^{n}a_{B_i}$ 。 另外从小到大枚举 $a_j<a_x$ 的 $b_j$ 最小值加入集合 $B$ ,直至超过 $m$ 。 时间复杂度 $O(N^2)$ ,考虑优化。 考虑将 $a_i,b_i$ 按照 $a_i$ 排序后进行上述操作。 可用数据结构快速维护,时间复杂度 $O(N \log N)$ 。 ### 反悔贪心视角 按 $a_i$ 排序,逐个加入 $i$ ,记录代价。 若代价 $>m$ 则选择 $b$ 最大的点删去直至代价 $\le m$ 。 循环过程中记录最大值。 ### 代码 ```cpp struct node{ int x,t; bool operator<(const node &p)const{ return x<p.x; } }s[100005]; priority_queue<int> q; int n,m,tot,ans; signed main(){ n=read(),m=read(); for(int i=1;i<=n;++i) s[i]={read(),read()}; sort(s+1,s+n+1); for(int i=1;i<=n;++i){ q.push(s[i].t); tot+=s[i].x-s[i-1].x+s[i].t; while(tot>m and q.size()) tot-=q.top(),q.pop(); int p=q.size(); if(tot<=m)ans=max(ans,p); } write(ans); return 0; } ``` ::: :::: ::::info[P3619 魔法] 给定 $f_0=T$ 和 $n$ 个 $(t_i,b_i)$ 数对,询问是否存在一种数列排列方式,使得 $f_i=f_{i-1}+b_i$ 满足 $\forall i\in [0,n],f_i>0$ 且 $\forall i\in [1,n],f_{i-1}>t_i$ 。 多测,组数不超过 $10$ 组。约定 $n,t_i,b_i,T\le 10^5$ 。 :::info[Hint] 考虑对正负分别贪心。 ::: :::info[sol] **贪心;邻项交换法推导;考虑对正负分类贪心。** - 性质: $b_i>0$ 的一定放前面。 - 证明: 如果存在 $b_j<0,b_i>0,j<i$ ,则交换 $i,j$ 一定更优。 所以 $\forall b_i>0, \nexists j<i, b_j<0$ 。 证毕。 - 性质:$\forall b_i>0,\nexists b_j>0,i<j,t_j<t_i

感性理解,对于贡献是正的元素,先来要求低的。

推一下 b_i<0 的情况,设有 b_1,b_2<0,t_1,t_2,f

(t_1,b_1) 放前面:f>t_1,f+b_1>t_2 ,即 f>max(t_1,t_2-b_1)

(t_2,b_2) 放前面:f>t_2,f+b_2>t_1 ,即 f>max(t_2,t_1-b_2)

如果把 (t_1,b_1) 放前面更好,则 max(t_1,t_2-b_1)<max(t_2,t_1-b_2)

考虑分类讨论拆掉 max 。

综上,如果把 (t_1,b_1) 放前面更好,则 t_1+b_1>t_2 ,缩放得 t_1+b_1>t_2+b_2

同理,如果把 (t_2,b_2) 放前面更好,则 t_2+b_2>t_1 ,缩放得 t_2+b_2>t_1+b_1

于是对于负数部分按照 t_i+b_i 从大到小排序即可。

给出代码。

struct node{int a,x;};
bool cmp1(node a,node b){return a.a<b.a;}
bool cmp2(node a,node b){return a.a+a.x>b.a+b.x;}
vector<node> s1,s2;
bool solve(){
    n=read(),t=read();
    s1.clear(),s2.clear();
    for(int i=1;i<=n;++i){
        int x=read(),y=read();
        if(y>=0)s1.push_back({x,y});
        else s2.push_back({x,y});
    }
    sort(s1.begin(),s1.end(),cmp1);
    sort(s2.begin(),s2.end(),cmp2);
    if(t<=0)return 0;
    for(auto x:s1){
        if(t<=x.a)return 0;
        t+=x.x;
    }
    for(auto x:s2){
        if(t<=x.a)return 0;
        t+=x.x;
        if(t<=0)return 0;
    }
    return 1;
}

::: ::::

未完工!

::::info[P4597 序列 sequence] 给定一个序列,每次操作把某个数 +1−1

把序列变成非降数列,求最少操作次数。

:::info[sol] **贪心;。** ::: :::: ::::info[P3111 [USACO14DEC] Cow Jog S] 有 $n$ 头奶牛在一个单人的超长跑道上慢跑,每头牛的起点位置都不同。 由于是单人跑道,所有他们之间不能相互超越。当一头速度快的奶牛追上另外一头奶牛的时候,他必须降速成同等速度。 把这些跑走同一个位置而且同等速度的牛看成一个小组。 请计算 $t$ 时间后,奶牛们将分为多少小组。 $N\le 10^5,~T\le 10^9$ ,保证数据起始坐标严格单调递增。 :::info[sol] **贪心;性质;思维。** 显然运动过程不改变相对位置关系。 定义 $s_i$ 为 $i$ 号牛经 $t$ 时间最终到达的位置,定义 $r_i$ 为 $i$ 号牛经 $t$ 时间后所在小组的最大编号。 快牛变慢的唯一途径是追上慢牛。 所以 $r_i=\begin{cases}r_i=r_{i+1}&s_i>s_{i+1}\\r_i=i&s_i\le s_{i+1} \end{cases}

对于第二种分类情况,对答案贡献 1

给出代码。

#define int long long
int n,t,s[100005],ans=1;
signed main(){
    cin>>n>>t;
    for(int i=1;i<=n;i++){
        int x,y;cin>>x>>y;
        s[i]=x+y*t;
    }
    for(int i=n-1;i>=1;i--)
        if(s[i]>=s[i+1])s[i]=s[i+1];
        else ans++;
    cout<<ans;
    return 0;
}

::: ::::

其他