Unique Snowflakes
题意
给定一组整数序列,求其中没有重复数字的最长区间长度。
双指针,集合思想
暴力代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
#include <iostream>
#include <set>
using namespace std;
const int maxn=2e6+5;
int n;
int a[maxn];
std::set<int> myset;
//暴力枚举以 a[j]为结尾的元素
void work() {
int ans = 0;
for(int j = 1;j <= n ;++j ) // j: 1->n
{
myset.clear();
for(int i = j;i >= 1 ;--i ) // i: j->1
{
if( myset.find(a[i]) != myset.end() ) break;
myset.insert(a[i]);
if( ans < myset.size())
ans = myset.size();
}
}
std::cout << ans << "\n";
}
int main() {
int T;
std::cin >> T;
while (T--) {
std::cin >> n;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
}
work();
}
return 0;
}
暴力优化
根据暴力代码进行优化,
- 那么
对于 是没有贡献的,同样对 也没有贡献 - 所以,可能对于
贡献的区间是 - 这具有单调性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <iostream>
#include <set>
using namespace std;
const int maxn=2e6+5;
int n;
int a[maxn];
std::set<int> myset;
//暴力枚举以 a[j]为结尾的元素
void work() {
int ans = 0;
myset.clear();
int i = 1;
for(int j = 1;j <= n ;++j ) // j: 1->n
{
while( myset.find(a[j]) != myset.end() )
{
myset.erase(a[i]);
i++;
}
myset.insert(a[j]);
if( ans < myset.size())
ans = myset.size();
}
std::cout << ans << "\n";
}
int main() {
int T;
std::cin >> T;
while (T--) {
std::cin >> n;
for(int i = 1;i <= n ;++i ) // i: 1->n
{
std::cin >> a[i];
}
work();
}
return 0;
}
详细的证明: 反证法
这个证明可以通过**“反证法”(Proof by Contradiction)或者“最优性归纳”**(Optimality Induction)来完成。
为了让你更容易记忆和理解,我推荐使用**“反证法”**的逻辑。核心思想是:假设我们错过了一个更长的答案,推导出这个假设本身是不成立的。
证明目标
我们要证明:算法计算出的最大长度 max_len,一定等于全局的最长无重复区间长度。
证明过程
1. 定义标准答案
假设全局真正的最长无重复区间是
2. 算法的行为
- 我们的右指针
从 遍历到 。这意味着,算法一定会在某个时刻让 到达 这个位置。 - 在该时刻,算法维护的左指针为
。 - 此时算法计算出的长度是
。
3. 反证假设
假设算法“略过”了最优解。
这意味着,当
4. 推导矛盾
既然
为什么
注意位置关系:
这意味着什么?
这意味着在区间
矛盾产生:
我们一开始定义
5. 结论
因此,假设不成立。
当
所以,必然有
通俗版总结(直觉理解)
- 右指针的地毯式搜索:右指针
会扫过数组的每一个位置,所以**任何一个可能的“终点”**都不会被漏掉。 - 左指针的“被迫”移动:左指针
是懒惰的。除非万不得已(也就是真的遇到了重复,不移不行了),否则它绝不向右动一步。 - 合体:既然每一个“终点”都试过了,而对于每一个终点,左指针都尽可能地“撑”到了最左边的极限位置。那么,每一个终点对应的“最长”区间其实都被我们算了一遍。最大值自然就在其中。
证明:最优性归纳法
使用 最优性归纳法 (Optimality Induction) 来证明,本质上是证明:在每一步迭代中,算法所维护的那个区间,都是以当前元素结尾的“局部最优解”。
如果我们能证明对于每一个位置
1. 定义状态
- 设
为输入数组。 - 设
为当算法处理到第 个元素时,左指针 的值。 - 设
为理论上以 结尾的最长无重复区间的起始下标。 - 也就是说,
是以 结尾的最长合法区间。
- 也就是说,
证明目标: 对于任意
2. 归纳证明过程
基础情况 (Base Case)
当
- 算法初始化
。由于之前没有任何元素,没有冲突, 。 - 理论上,区间
只有一个元素,不可能重复,起始点 。 - 结论:
成立。
归纳步骤 (Inductive Step)
假设:对于
考虑当前元素
我们需要分两种情况讨论:
情况 A:
- 理论推导 (
): 既然 不在之前的区间 中出现,那么加上 后,整个区间依然是无重复的。为了让区间最长,左边界应该保持不变。 。 - 算法行为 (
): 算法检查 map,发现小于当前 ,不触发更新(或 max取值取了原来的)。 。 - 匹配结果:两者相等。
情况 B:
- 理论推导 (
): 区间 是合法的,但加上 后,与位置 的元素重复了。 为了消除重复并保持以 结尾: - 新的左边界必须在
的右边(否则包含两个 )。 - 新的左边界不能比
更靠右(因为我们要找最长区间,且 内部本身就是无重复的,加上 后也没问题)。 理论上的最优左边界必须跳到冲突点的下一位: 。
- 新的左边界必须在
- 算法行为 (
): 算法检测到 ,执行 L = last_pos + 1。。 - 匹配结果:两者相等。
3. 结论
- 通过归纳法证明了:算法维护的
指针,始终是“以当前右指针结尾的最长无重复子串”的理论最优左边界。 - 既然算法正确计算了每一个位置
对应的最长区间长度 。 - 那么,算法过程中记录的
max_len = max(Len(0), Len(1)... Len(n-1)),必然就是全局的最优解。
证毕。