技巧整理
Believe_in_dreams
·
2025-11-30 14:58:57
·
算法·理论
考试技巧总结
注意事项
详细内容见 警示自己 。
考试策略
:::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$ 必选,然后再横向考虑选择一些其他节点。

组合计数的不重不漏是由 **横纵坐标互不相同** 保证的。
:::
::::
::::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)\}
转移时间复杂度太高。
打表/数学归纳法/线性基知识,都可以发现一个关键性质:对于所有 u ,SG(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
则输出最大数。
算法正确性证明
如果序列只有非负数。
显然 0 没有任何作用。
性质: 最大数一定放在第一个是最优的。
证明:
因为所有数字都为正数,所以从 f_2 起所有数不被计入贡献。
所以只能选择一个数字,最大数一定最优,得证。
如果序列只有非正数。
显然 0 没有任何作用。
性质: 最大数一定放在第一个是最优的。
证明:
因为所有数字都为负数,所以从 f_2 起所有数不被计入贡献。
所以必须选择一个数字,最大数一定最优,得证。
如果序列有正有负。
我们规定 “ x 被使用” 指的是在计算 f 的过程中到达 x 时,x 与 f_{i-1} 异正负,进入函数判定的第二个部分并对 f_i 给出贡献。
证明:
因为所有使用的数字在合法情况下交换顺序不改变结果。
又因为一定至少有一个负数被使用。
而将该数提前到最初使用一定合法,并且如此会导致 f_1<0 ,所以得证。
证明:
考虑反证,显然 0 在此处不会造成任何影响。
故得证。
性质: 最优排列中最后一个使用的数一定是最大的正数。
证明:
故得证。
性质: 一定存在一种去除最大数的排列经过 f 计算的最后结果等于使用任意 非最大数 凑出的 “最大的小于 0 的数”。
证明:
考虑反证。
如果 f 在某时 >0 且应使用的数中只剩下 >0 的数
则所有使用的数的总和 >0 ,即凑出的数 >0 ,与假设“凑出的数 <0 ”矛盾。
如果 f 在某时 <0 且应使用的数中只剩下 <0 的数
则不使用这些剩余的数字一定使得凑出的数 <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$ 连边一定不优秀。

考虑证明,设与某黄色点 $k$ 连了边。
能找到绿色点 $j$ 使得 $x_j,x_k$ 在同时 $>x_i$ 或者同时 $<x_i$ 。
- 情况 $1$ :若 $i$ 与 $j$ 有连边,如下图所示。

则连边 $i\to j,~j\to k$ 一定更短且合法,如下图所示。

- 情况 $2$ :若 $i$ 与 $j$ 无连边,如下图所示。

则根据勾股定理,将连边 $i\to k$ 改为 $i\to j$ 一定更优,如下图所示。

即证毕。
```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 。
$\text{原式}\Rightarrow t_1<t_1-b_2$ ,恒成立。
$\text{原式}\Rightarrow t_2-b_1<t_2$ ,恒矛盾。
综上,如果把 (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;
}
:::
::::
其他