1、枚举
1.1、思想
枚举法的本质就是从所有候选答案中去搜索正确的解
//假设解空间(候选答案范围)为[l,r]
for(int i=l; i<=r; i++)
{
if(check(i)) //根据题目条件判断是否成立
{
//输出解或统计可行解的数量...
}
}
优化枚举的基本思路—减少枚举次数
- 选择合适的枚举对象
- 选择合适的枚举方向(方便排除非法和不是最优的情况)
- 选择合适的数据维护方法(转化问题)
1.2、常用方法
- 前缀和
- 尺取法
- 差分
1.2.1、前缀和思想
问题引入
给定一个数列{an} ,
(
1
≤
n
≤
100000
)
(1\leq n \leq 100000)
(1≤n≤100000),有q
(
1
≤
q
≤
100000
)
(1\leq q \leq 100000)
(1≤q≤100000)次询问,每次询问数列的第
l
i
l_i
li个元素到第
r
i
r_i
ri个元素的和。
主要的时间复杂度瓶颈在哪里
对区间的查询需要将整个区间扫一遍
我们可以用 s u m [ i ] sum[i] sum[i]存储前 i i i个数的和,那么 s u m [ i ] = s u m [ i − 1 ] + a [ i ] sum[i] = sum[i-1] + a[i] sum[i]=sum[i−1]+a[i],当我们要查询第 l i l_i li个元素到第 r i r_i ri个元素的和时,用 s u m [ r i ] − s u m [ l i − 1 ] sum[r_i] - sum[l_i-1] sum[ri]−sum[li−1]即可。
• 这样单次查询的复杂度是
O
(
1
)
O(1)
O(1)的,总复杂度是
O
(
n
+
q
)
O(n+q)
O(n+q)
• 这里的
s
u
m
[
i
]
sum[i]
sum[i]是前缀和,前缀和是算法竞赛当中非常常用的小技巧
1.2.2、差分思想
问题引入
给定一个数列{an} ,
(
1
≤
n
≤
100000
)
(1\leq n \leq 100000)
(1≤n≤100000),有q
(
1
≤
q
≤
100000
)
(1\leq q \leq 100000)
(1≤q≤100000)次修改,每次把数列中的第
l
i
l_i
li到第
r
i
r_i
ri的每个元素都加上一个值
k
i
k_i
ki,求所有的修改之后每个数的值。
主要的时间复杂度瓶颈在哪里?
对区间的修改需要将整个区间扫一遍,时间复杂度O(n * q)
- 考虑如何进行转化 — 将对区间的修改变为对区间端点的修改
- 考虑在区间加的过程中有什么值是在区间端点处发生了变化而区间内是没有变化的 — 每个数与其前一个数的差值
当我们对第 l i l_i li 个到第 r i r_i ri 个数加上 k i k_i ki 时,第 l i l_i li 个数与第 l i − 1 l_i-1 li−1 个数的差值增加了 k i k_i ki ,第 r i + 1 r_i+1 ri+1 个数与第 r i r_i ri 个数的差值减少了 k i k_i ki ,而区间内部的相邻两个数的差值是不变的。
用一个数组 d e l t a [ i ] delta[i] delta[i] 来维护第i个数和其前一个数的差值(可以默认第一个数前面有一个0),然后当需要将 [ l i , r i ] [l_i,r_i] [li,ri] 区间的每一个数 + k i k_i ki 时,只需要修改 d e l t a [ l i ] delta[l_i] delta[li] 和 d e l t a [ r i + 1 ] delta[r_i+1] delta[ri+1] 即可。
在所有的修改操作进行完之后,对 d e l t a [ i ] delta[i] delta[i] 求一次前缀和,就可以得到数列的每个元素的值。
差分和前缀和是一对对称的操作(即对差分数组求前缀和就是原数组,对前缀和求差分也会得到原数组)。
1.3、例题
最大正方形
在一个 N ∗ N ( N ≤ 100 ) N*N(N\leq100) N∗N(N≤100)矩阵中求一个最大的正方形使得该正方形的四个顶点都是有字符’#'构成
#*#***
******
#*#*#*
******
#*****
***#**
考虑:几个点能确定一个正方形?
- 四个点能确定一个正方形
- 三个点能确定一个正方形
- 两个点的情况:
-
- 两个点为水平或垂直线上不能确定一个唯一的正方形
-
- 两个点为对角线上能确定一个正方形
- 两个点为对角线上能确定一个正方形
校门外的树
某校大门外长度为L的马路上有一排树,每两棵相邻的树之间的间隔都是1米。我们可以把马路看成一个数轴,马路的一端在数轴0的位置,另一端在L的位置;数轴上的每个整数点,即0,1,2,…,L都种有一棵树。
由于马路上有一些区域要用来建地铁。这些区域用它们在数轴上的起始点和终止点表示。已知任一区域的起始点和终止点的坐标都是整数,区域之间可能有重合的部分。现在要把这些区域中的树(包括区域端点处的两棵树)移走。你的任务是计算将这些树都移走后,马路上还有多少棵树。
- 原题数据范围:
1
≤
L
≤
10000
和
1
≤
M
≤
100
1≤L≤10000和1≤M≤100
1≤L≤10000和1≤M≤100
暴力 - 扩大数据范围:
1
≤
L
≤
100000
和
1
≤
M
≤
100000
1≤L≤100000和1≤M≤100000
1≤L≤100000和1≤M≤100000
差分 - 继续增大数据范围:
1
≤
L
≤
1
0
9
和
1
≤
M
≤
100000
1≤L≤10^9和1≤M≤100000
1≤L≤109和1≤M≤100000
离散化
Subsequence(POJ - 3061)
给定长度为n的整数数列以及整数S,求出总和不小于S的连续子串的长度的最小值,如果解不存在,输出0
常规枚举 —
O
(
n
3
)
O(n^3)
O(n3)
前缀和 —
O
(
n
2
)
O(n^2)
O(n2)
尺取/追逐/Two-Pointer —
O
(
n
)
O(n)
O(n)
void solve()
{
int res=N+1;
int s=0,t=0,sum=0;
for(;;)
{
while(t<N&&sum<S)
{
sum+=a[t++];
}
if(sum<S)
break;
res=min(res,t-s);
sum-=a[s++];
}
if(res>N)
res=0;//解不存在
printf("%d\n",res);
}
Flip Game
有一个 N ∗ M N*M N∗M的灯泡, ( N ≤ 10 , M ≤ 100 ) (N \leq10,M\leq100) (N≤10,M≤100),每次按某一个点可以使得其本身以及其上下左右共五个的灯的开关反向。给定初始状态(每个灯泡的亮或者灭),问:能否把所有灯都灭掉?