题解:洛谷 P1638 逛画展
【题目来源】
【题目描述】
博览馆正在展出由世上最佳的 \(m\) 位画家所画的图画。
游客在购买门票时必须说明两个数字,\(a\) 和 \(b\),代表他要看展览中的第 \(a\) 幅至第 \(b\) 幅画(包含 \(a,b\))之间的所有图画,而门票的价钱就是一张图画一元。
Sept 希望入场后可以看到所有名师的图画。当然,他想最小化购买门票的价格。
请求出他购买门票时应选择的 \(a,b\),数据保证一定有解。
若存在多组解,输出 \(a\) 最小的那组。
【输入】
第一行两个整数 \(n,m\),分别表示博览馆内的图画总数及这些图画是由多少位名师的画所绘画的。
第二行包含 \(n\) 个整数 \(a_i\),代表画第 \(i\) 幅画的名师的编号。
【输出】
一行两个整数 \(a,b\)。
【输入样例】
12 5
2 5 3 1 3 2 4 1 1 5 4 3
【输出样例】
2 7
【核心思想】
-
问题分析:给定一个长度为 \(n\) 的序列,每个位置属于 \(m\) 个画家之一。需要找到一个最短的连续子区间,使得该区间包含所有 \(m\) 种画家编号。若有多个长度相同的最短区间,输出左端点最小的那个。本质是最小覆盖子串问题,可以用双指针滑动窗口高效求解。
-
算法选择:
- 双指针滑动窗口:维护一个窗口
[l, r],用计数数组cnt记录窗口内每种画家出现的次数,用变量kind记录窗口内不同画家的种类数。右指针r不断向右扩展,当kind == m时,说明窗口已包含所有画家,此时尝试右移左指针l来收缩窗口,并在收缩过程中更新最优解(记录更短的区间,若长度相同则取左端点更小的)。继续重复直到右指针到达末尾。 - 复杂度:\(O(n)\),每个元素最多被左右指针各访问一次。
- 双指针滑动窗口:维护一个窗口
-
关键步骤:
- 读入 \(n, m\) 和序列 \(a[1..n]\)。
- 初始化左指针
l = 1,右指针r = 0,kind = 0,计数数组全 0,最优长度bestLen = INF,最优左右端点ansL, ansR。 - 当
r < n时循环:- 右移
r++,将a[r]加入窗口:cnt[a[r]]++,若cnt[a[r]] == 1则kind++。 - 当
kind == m时:- 尝试移动左指针
l:循环条件为l <= r且当前窗口仍包含所有颜色(即kind == m):- 若当前区间长度
r - l + 1 < bestLen,则更新bestLen = r - l + 1,ansL = l, ansR = r。 - 移除
a[l]:cnt[a[l]]--,若cnt[a[l]] == 0则kind--,l++。
- 若当前区间长度
- 尝试移动左指针
- 右移
- 输出
ansL ansR。
-
时间/空间复杂度:
- 时间复杂度:\(O(n)\),左右指针各移动 \(O(n)\) 次。
- 空间复杂度:\(O(m)\),用于计数数组。
-
滑动窗口的核心思想:
- 单调性:窗口内的种类数随右指针增大而单调不减,随左指针增大而单调不增。
- 最优更新:在窗口满足条件时,不断收缩左边界,尝试找到更短的可行区间,且由于左指针从左到右移动,首先遇到的等长最短区间其左端点自然最小。
- 适用场景:适用于需要寻找最短连续子数组满足某种“覆盖”条件的问题,是双指针的经典应用。
【算法标签】
普及 #双指针
【代码详解】
#include <bits/stdc++.h>
using namespace std;
const int MAX_N = 1000005; // 定义数组最大长度
const int MAX_M = 2005; // 定义颜色种类最大数量
int n, m; // n: 珠子数量, m: 颜色种类数
int a[MAX_N]; // 存储珠子颜色序列
int cnt[MAX_M]; // 记录当前窗口中各颜色出现次数
int num; // 记录当前窗口中不同颜色数量
int len = 1e9; // 记录满足条件的最短长度(初始设为极大值)
int l, r; // 记录最优解的左右边界
int main()
{
// 输入珠子数量和颜色种类数
cin >> n >> m;
// 输入珠子颜色序列
for (int i = 1; i <= n; i++)
cin >> a[i];
// 初始化滑动窗口指针和计数器
int i = 1, j = 1; // i: 窗口左边界, j: 窗口右边界
cnt[a[1]] = 1; // 初始化第一个珠子的颜色计数
num = 1; // 当前窗口包含1种颜色
// 滑动窗口法寻找最短包含所有颜色的子串
while (j <= n)
{
// 如果当前颜色种类不足,扩大右边界
if (num < m)
{
j++; // 右边界右移
cnt[a[j]]++; // 新颜色计数增加
// 如果是新出现的颜色,增加颜色计数
if (cnt[a[j]] == 1)
num++;
}
// 如果当前窗口包含所有颜色
if (num == m)
{
// 更新最优解
if (len > j - i + 1)
{
len = j - i + 1; // 更新最短长度
l = i; // 记录左边界
r = j; // 记录右边界
}
// 尝试缩小左边界
cnt[a[i]]--; // 左边界的颜色计数减少
// 如果某种颜色数量减为0,减少颜色计数
if (cnt[a[i]] == 0)
num--;
i++; // 左边界右移
}
}
// 输出最优解的左右边界
cout << l << " " << r;
return 0;
}
// 使用acwing模板二刷
#include <bits/stdc++.h>
using namespace std;
const int N = 1000006; // 定义数组最大长度
const int COLOR_MAX = 2005; // 定义颜色种类最大数量
int n, m; // n: 珠子数量, m: 颜色种类数
int a[N]; // 存储珠子颜色序列
int cnt[COLOR_MAX]; // 记录当前窗口中各颜色出现次数
int num; // 记录当前窗口中不同颜色数量
int len = 1e9; // 记录满足条件的最短长度(初始设为极大值)
int l, r; // 记录最优解的左右边界
int main()
{
// 输入珠子数量和颜色种类数
cin >> n >> m;
// 输入珠子颜色序列
for (int i = 1; i <= n; i++)
cin >> a[i];
// 初始化滑动窗口和计数器
cnt[a[1]] = 1; // 第一个珠子的颜色计数
num = 1; // 当前窗口包含1种颜色
// 滑动窗口法寻找最短包含所有颜色的子串
for (int i = 1, j = 1; j <= n; i++) // i:左边界, j:右边界
{
// 当颜色种类不足时,扩大右边界
while (num < m && j <= n)
{
j++; // 右边界右移
cnt[a[j]]++; // 新颜色计数增加
// 如果是新出现的颜色,增加颜色计数
if (cnt[a[j]] == 1)
num++;
}
// 如果当前窗口包含所有颜色
if (num == m)
{
// 更新最优解
if (len > j - i + 1)
{
len = j - i + 1; // 更新最短长度
l = i; // 记录左边界
r = j; // 记录右边界
}
// 尝试缩小左边界
cnt[a[i]]--; // 左边界的颜色计数减少
// 如果某种颜色数量减为0,减少颜色计数
if (cnt[a[i]] == 0)
num--;
}
}
// 输出最优解的左右边界
cout << l << " " << r;
return 0;
}
#include <bits/stdc++.h>
using namespace std;
const int N = 1000005; // 定义常量N,最大数组大小
int n, m, num, l, r, ans; // n: 数组长度, m: 窗口大小(代码中未使用), num: 未使用, l,r: 结果区间的左右端点, ans: 当前最小长度
int a[N], cnt[2005]; // a: 原始数组, cnt: 计数数组(记录每个数字出现的次数)
struct Node
{
int a, idx; // a: 数字值, idx: 在数组中的索引位置
};
deque<Node> dq; // 双端队列,用于存储当前窗口的元素
// 从队列尾部弹出重复元素
void popBack()
{
while (dq.size()) // 当队列不为空时
{
int t = dq.back().a; // 获取队尾元素的数值
if (cnt[t] > 1) // 如果这个数字在数组中出现了多次(至少2次)
{
dq.pop_back(); // 从队尾弹出这个元素
cnt[t]--; // 对应数字的计数减1
}
else
break; // 如果这个数字只出现一次,则停止弹出
}
}
// 从队列头部弹出重复元素
void popFront()
{
while (dq.size()) // 当队列不为空时
{
int t = dq.front().a; // 获取队首元素的数值
if (cnt[t] > 1) // 如果这个数字在数组中出现了多次(至少2次)
{
dq.pop_front(); // 从队首弹出这个元素
cnt[t]--; // 对应数字的计数减1
}
else
break; // 如果这个数字只出现一次,则停止弹出
}
}
int main()
{
cin >> n >> m; // 输入数组长度n和窗口大小m(但代码中m未使用)
// 读取数组元素并初始化
for (int i = 1; i <= n; i++)
{
cin >> a[i]; // 读取第i个元素
cnt[a[i]]++; // 记录数字a[i]出现的次数
dq.push_back({a[i], i}); // 将元素和索引加入队列
}
// 初始处理:从两端弹出重复元素
popBack(); // 从尾部弹出重复元素
popFront(); // 从头部弹出重复元素
// 初始化结果
ans = dq.size(); // 当前队列长度作为初始答案
l = dq.front().idx; // 记录左端点索引
r = dq.back().idx; // 记录右端点索引
// 滑动窗口处理
for (int i = r + 1; i <= n; i++) // 从当前右端点+1开始
{
dq.push_back({a[i], i}); // 将新元素加入队尾
cnt[a[i]]++; // 更新计数
popFront(); // 从头部弹出重复元素(如果有)
// 如果当前队列长度更小,更新结果
if (dq.size() < ans)
{
ans = dq.size(); // 更新最小长度
l = dq.front().idx; // 更新左端点
r = dq.back().idx; // 更新右端点
}
}
// 输出结果
cout << l << " " << r << endl; // 输出最小长度的区间端点
return 0;
}
【运行结果】
12 5
2 5 3 1 3 2 4 1 1 5 4 3
2 7
浙公网安备 33010602011771号