代码宏定义以及框架约定

#include <bits/stdc++.h>
using namespace std;
#define IOS ios_base::sync_with_stdio(0);cin.tie(0);cout.tie(0);
// #define int long long
#define endl '\n'
#define STED(_x) _x.begin(), _x.end()
#define rep(_x, _y, _z) for (int _x = _y; _x < _z; _x++)
typedef long long i64;
typedef pair<int, int> pii;
const int N = 2e5 + 10;
// dont use umap!!! signed main()
{
IOS;
int _ = 1;
//cin >> _; while (_--) solve(); return 0;
}

Anagram Search

题意简述

两个字符串 \(s\) 和 \(t\) 相似的定义为:\(s\) 可以打乱之后重新排列成为 \(t\)。题目给出\(a\) 和 \(b\),问 \(a\) 中有多少子串(连续的一段)与 \(b\) 相似。

同时,\(a\) 中还含有 \(?\) 字符,他可以等价于任何字符(可以变成任何字符)

解题思路

实际上,根据题目对于相似的定义,我们可以知道,对于任意字符串\(s\)和\(t\),只要满足长度相等,且组成的字母对应的个数相同,就可以认为他们相似。

因此,我们可以使用一个特殊技巧: 滑动窗口。对于字符串 \(a\) ,我们每次维护一个长度为 \(len(b)\) 的窗口,每次一个一个单位地移动窗口,每移动一次就统计窗口中字母的个数,并与 \(b\) 中的字母个数进行比较,如果相同,则说明窗口中的子串与 \(b\) 相似。

关于比较:首先,我们可以预处理出 \(b\) 中每个字母出现的次数,用 \(cnt\) 来记录 \(b\) 中出现的字母个数。然后,同样使用数组 \(cnt_a\) 来记录窗口中字母出现的次数。但是,由于 \(a\) 中 \(?\) 的存在,我们每次比较 \(cnt\) 和 \(cnt_a\) 的时候,由于窗口长度和\(b\) 长度相同,所以只需要满足 $ {\forall}cnt_a[i] <= cnt[i]$,二者即相似,因为 \(?\) 可以等价于任何字符,\(cnt_a\)少于\(cnt\)的部分就是 \(?\) 的数量。

复杂度分析

预处理 \(b\) 中每个字母出现的次数,时间复杂度为 \(O(n)\),其中 \(n\) 为 \(b\) 的长度。

滑动窗口,时间复杂度为 \(O(n)\),其中 \(n\) 为 \(a\) 的长度。

每次比较,由于循环次数为常数26,时间复杂度为 \(O(1)\)。

因此,总时间复杂度为 \(O(n)\)。

AC代码

void solve()
{
string s, t;
cin >> s >> t;
vector<int> cnt(26), vcnt(26); //cnt记录b中字母个数,vcnt记录窗口中字母个数
int ans = 0;
for (auto i : t) //预处理字符串t
{
cnt[i - 'a']++;
}
for (int i = 0; i < s.size(); i++)
{
if (s[i] != '?') //向右移动一个单位,加入右边的新字符
vcnt[s[i] - 'a']++;
if (i >= t.size() && s[i - t.size()] != '?') //同时弹出最左边的一个字符
vcnt[s[i - t.size()] - 'a']--;
if (i >= t.size() - 1)
//当窗口长度为t的长度时,开始判断窗口中的子串是否与t相似
{
bool ok = 1;
for (int i = 0; i < 26; i++)
{
if (vcnt[i] > cnt[i]) //如果窗口内某个字母的个数大于b中该字母的个数,则二者不相似
{
ok = 0;
break;
}
}
ans += ok;
}
}
cout << ans << endl;
}

Exposition

题目简述

给一个 \(n\) 个元素的序列,从中挑出最长的子串,要求子串中元素差的最大值不超过 \(k\)

思路分析

既然要求任意两个数之差不超过 \(k\) 的最长子串,实际上是求最大极差,考虑暴力的做法,那么我们可以考虑枚举子串的起点,从 \(1 ~ n\) 的每一个点开始,利用双指针维护一个子串,一个指针指向起点,另一个从起点开始向后枚举,维护双指针所覆盖区间的极差,直到遇到一个极差大于 \(k\) 的点,此时记录当前子串的长度,并更新答案,然后继续枚举下一个起点,直到枚举完所有起点。

暴力做法,每一次从一个点开始向后枚举,复杂度为 \(O(n)\),序列一共有 \(n\) 个点,所以总的时间复杂度为 \(O(n^2)\)。而此题数据范围为 \(n ≤ 10^5\),而 \(n^2 = 10^{10}\),所以暴力做法会超时。

以此为思路,我们考虑暴力法中有那些步骤可以优化,我们发现,枚举 \(n\) 个起点似乎已经无法优化了,而每一次从该起点向后枚举,维护双指针所覆盖区间的极差,似乎可以优化。

首先,由于我们起点确定,记为 \(a_{st}\),需要维护的是终点,假设存在 \(a_i\),使得 \(a_i - {\forall}a_{st~i} \geq k\),那么对于 \(a_i\) 以及之后的数,我们一定不会将他们选进子串中,由此我们可以发现,我们维护的极差具有单调性!同时,求小于 \(k\) 的最大极差,同时又有单调性,那不就可以采用二分搜索了吗?那么我们每次枚举的复杂度就降到了 \(O(\log_2 n)\),但是,枚举的时间复杂度降低了,我们还有一个问题,如何快速求出每次询问区间的极差?这就得请我们的区间操作神器--线段树登场了!,我们可以对原序列建立线段树,维护区间极差,这样每次时间复杂度就降到了 \(O(\log_2 n)\),总的时间复杂度就降到了 \(O(n\log_2 n)\),而 \(n ≤ 10^5\),所以总的时间复杂度为 \(O(n\log_2 n)\),可以接受。

时间复杂度分析

在原序列上建立线段树,复杂度为 \(O(n)\)。

枚举 \(n\) 个起点,复杂度为 \(O(n)\)。

对于每次枚举的起点,我们用二分搜索,会产生 \(O(\log_2 n)\) 次询问,而线段树上每次查询时间复杂度为 \(O(\log_2 n)\),因此枚举枚举的总复杂度为 \(O(\log_2 n)^2\)。

因此总的时间复杂度为 \(O(n\log_2 n)\)。

AC代码

vector<int> a;

struct node //线段树节点
{
int l, r, max, min;
} tr[N << 2]; //线段树需要4倍的空间 void update(int u) //从子节点更新当前节点
{
tr[u].max = max(tr[u << 1].max, tr[u << 1 | 1].max);
tr[u].min = min(tr[u << 1].min, tr[u << 1 | 1].min);
} void build(int u, int l, int r) //建立线段树
{
tr[u] = {l, r, a[r], a[r]};
if (l == r)
return;
int mid = l + r >> 1;
build(u << 1, l, mid);
build(u << 1 | 1, mid + 1, r);
update(u);
} node query(int u, int l, int r) //区间查询
{
if (l <= tr[u].l && tr[u].r <= r)
return tr[u];
int mid = tr[u].l + tr[u].r >> 1;
if (r <= mid)
return query(u << 1, l, r);
else if (l > mid)
return query(u << 1 | 1, l, r);
else
{
auto L = query(u << 1, l, r), R = query(u << 1 | 1, l, r);
node res;
res.max = max(L.max, R.max);
res.min = min(L.min, R.min);
return res;
}
} int query(int l, int r) //区间查询
{
auto p = query(1, l, r);
return p.max - p.min; //返回极值
} void solve()
{
IOS;
int n, k;
cin >> n >> k;
a.resize(n + 1);
rep(i, 1, n + 1) cin >> a[i];
build(1, 1, n); //建立线段树
int len = 0, cnt = 0;
vector<pii> st;
rep(i, 1, n + 1)
{
int l = i, r = n;
while (l < r) //二分查找
{
int mid = l + r + 1 >> 1;
if (query(i, mid) <= k) //如果区间极差小于k
l = mid;
else
r = mid - 1;
}
if (r - i + 1 > len) //如果找到了一个更长的子串
{
len = r - i + 1, cnt = 1; //更新答案
st.clear(); //舍弃之前所有较短的子串
st.push_back({i, r}); //存入答案
}
else if (r - i + 1 == len) //如果找到一个等于最长串的子串,把他存入
cnt++, st.push_back({i, r});
}
cout << len << " " << cnt << endl; //输出答案
for (auto [l, r] : st)
cout << l << " " << r << endl;
}

Music in Car

题意简述

给定两个长度为 \(n\) 的序列,其中 \(a_i\) 表示第 \(i\) 个元素的贡献,\(b_i\) 表示获得第 \(i\) 个元素的代价。

同时,给出 \(n,w,k\) 分别代表序列长度,操作次数以及最大代价,对于每次操作,你可以任意挑选一个位置 \(i\),让他的代价 \(b_i\) 变成 \(\lceil \frac{b_i}{2} \rceil\)。

你可以选定任意一段从 \(i\) 开始,\(j\) 结束的序列,满足\(\sum b_{i~j} \leq k\),并得到 \(\sum a_{i~j}\) 的贡献。

求出最大贡献。

思路分析

对于任意一个区间,我们拥有 \(w\) 次操作机会,根据贪心的思想,我们每次选择代价最大的位置进行操作,这样我们就一定可以得到最大的贡献。

因此,类似于滑动窗口的思想,我们可以枚举区间右端点 \(r\),让指针 \(r\) 向右一直前进,同时维护一个左端点指针\(l\),每次移动完 \(r\) 后,我们就检查是否有 \(\sum b_{l~r} > k\) ,若出现这种情况,则不断向前移动左指针 \(l\),并不断删除最左边的数,直到 \(\sum b_{l~r} \leq k\),这样我们就可以维护一个区间,满足条件,每次完成上述操作后更新答案。

对于 \(w\) 次操作,我们每次选择代价前 \(w\) 大的位置进行操作(打折),而求前 \(w\) 大正好可以采用平衡树来维护,这里并不需要我们手写平衡树,cpp的STL中就自带了一个红黑树multiset,利用muiltset,我们可以很轻松地维护数之间的大小关系。

tip: muiltset存储数据天然有序,muiltset的最前面的元素一定是最小的元素,最后面的一定是最大的元素

在区间扫描的过程中,我们可以使用两个平衡树来维护打折区间 \(L\) 以及原价区间 \(H\),右端点每次扫过一个数,我们可以进行如下操作

  1. 首先,将新的数的代价\(b_i\)加入到\(L\)中,总贡献加上\(a_i\),总代价加上\(\lceil \frac{b_i}{2} \rceil\)
  2. 如果\(L\)中的数超过了 \(w\) 个,那么我们删除\(L\)中代价最小的数(即\(L_1\)),总代价减去\(\lceil \frac{L_1}{2} \rceil\),并加上\(L_1\),同时将弹出来的数加入到\(H\)中
  3. 检查总代价 \(p\) 是否超过了 \(k\),若超过,则应该移除 \(l\) 指针的数,并将其向左移动一个单位

关于弹出 \(l\) 指针对应的数:首先,我们应该检查\(b_i\)是否存在于打折区间 \(L\) 中(即比较\(L_1\) 和 \(b_i\) 的大小,由于我们打着区间中存放的是前 \(w\) 大的数,若\(b_l\)大于打着区间中最小的数(\(b_l \geq L_1\)),则\(b_l\)一定存在于打折区间中),我们从\(L\)中删除\(b_i\),同时总贡献减掉 \(\lceil \frac{b_l}{2} \rceil\),删除 \(b_l\) 后,打折区间多出一个空位,我们让原价区间中最大的数( \(H_{H.size()-1}\) )加入到打折区间中,总代价减掉 \(b_{H_{H.size()-1}}\) 并加上 \(\lceil \frac{b_{H_{H.size()-1}}}{2} \rceil\)。如果 \(b_l\) 不存在于打折区间,那么我们直接从原价区间中删掉 \(b_l\) 并将贡献减去 \(b_l\) 即可。最后将总贡献减掉 \(a_l\)。重复上述步骤,不断左移动 \(l\) 指针,直到总代价不超过 \(k\) 为止。

  1. 更新答案

时间复杂度分析

枚举右端点 \(r\),复杂度为 \(O(n)\)

维护左端点 \(l\),每次移动 \(l\) 指针,复杂度为 \(O(n)\)

红黑树维护打折区间 \(L\) 和原价区间 \(H\),每次插入一个数,删除一个数,复杂度为 \(O(\log n)\)

总时间复杂度为 \(O(n^2 \log n)\)

AC代码

void solve()
{
int n,w,k;
cin >> n >> w >> k;
vector<int> a(n+1),b(n+1);
rep(i, 1, n + 1) cin >> a[i];
rep(i, 1, n + 1) cin >> b[i];
int l = 1, p = 0, all = 0 ,ans = 0; //p为当前区间总代价,all为总贡献
multiset<int> H,L; //H存原时长,L存折半
for(int i = 1; i<= n; i++)
{
L.insert(b[i]); //将新的数加入打折区间
p += b[i]+1>>1; //累加贡献和代价
all += a[i];
if(L.size()>w) //如果打折区间超过w个数,弹出代价最小的数
{
H.insert(*L.begin()); //并将他加入到原价区间中
p += *L.begin(); //处理代价
p -= (*L.begin()+1) >> 1;
L.erase(L.begin()); //弹出
}
while(p > k) //如果当前区间的总代价p超过k,则需要弹出左端点
{
if(b[l] >= *L.begin()) //如果左端点对应的代价在打折区间内
{
p -= (b[l]+1)>>1; //处理代价
L.erase(L.find(b[l])); //从打折区间删除
if(H.size()) //如果原价区间中有数,则把其中最大的那一个弹出并加入到打折区间
{
L.insert(*H.rbegin()); //rbegin相当于末尾的指针
p -= *H.rbegin();
p += (*H.rbegin()+1)>>1;
H.erase(H.find(*H.rbegin()));
}
}
else //如果在原价区间,直接操作即可
{
p -= b[l];
H.erase(H.find(b[l]));
}
all -= a[l]; //减掉被弹出左端点的贡献
l++; //左移指针
}
ans = max(ans,all);//更新答案
}
cout << ans << endl;
}

Travel Card

题意简述

为了乘车,有三种票可买:

  1. 花 \(20\) 元购买单程票;
  2. 花 \(50\) 元购买 \(90\) 分钟内任意乘车的票;
  3. 花 \(120\) 元购买 \(1440\) 分钟内任意乘车的票。

你计划乘 \(n\) 次车,第 \(i\) 次乘车开始于第 \(t_i\) 分钟的开头且恰好持续一分钟。

请计算出完成前 \(i\) 次乘车需要的最少金额。

解题思路

这是一个经典的背包DP问题,我们做出如下约定

\(dp[i]\) 表示第 \(i\) 次所需要的最小金额(选择最优的购票方式)

对于每次乘车,我们用三种状态转移方式

  1. 选择第一种购票方式,我们可以直接从上一次乘车的地方购买单程票到这次的地方,花费20元,即为
\[\begin{equation}
dp[i] = dp[i-1] + 20
\nonumber
\end{equation}
\]
  1. 选择第二种购票方式,我们可以从 \(90\) 分钟内到过的任何地方,购买一张 \(90\) 分钟的票,到达这次的地方,根据贪心的思想,我们应该选择让乘车时间尽可能长,也就是离我们这里越远的地方越好,由于我们在上一次购票后花费\(1\)分钟进行乘车,因此这一分钟也要算在时间内,因此公式为
\[\begin{equation}
dp[i] = dp[t] + 50 (a_i - a_t \le 89 且 a_t \le \forall (a_i-89) ~ a_i )
\nonumber
\end{equation}
\]
  1. 选择第三种购票方式,其贪心思路等价于第二种购票方式。
\[\begin{equation}
dp[i] = dp[t] + 120 (a_i - a_t \le 1439 且 a_t \le \forall (a_i-1439) ~ a_i )
\nonumber
\end{equation}
\]

最后,我们得到了三种状态转移方程,我们从中选择一种最小的方案完成转移即可。

由于题目给出的序列保证了有序,因此对于找到大于 \(a_i - 89\) 的最小值,我们可以使用二分查找来优化。

复杂度分析

对于每次状态转移,我们需要用二分查找找到大于 \((a_i - 89) 和 (a_i - 1439)\) 的最小值,时间复杂度为 \(O(\log n)\)。

我们一共要进行 \(n\) 次状态转移,因此总时间复杂度为 \(O(n \log n)\)。

AC代码

void solve()
{
int n;
cin >> n;
vector<int> a(n + 1), dp(n + 1);
rep(i, 1, n + 1) cin >> a[i];
rep(i, 1, n + 1)
{
//三种状态转移中取最小值
dp[i] = min({dp[i-1]+20,
dp[lower_bound(a.begin()+1,a.begin()+1+i,a[i] - 89) - a.begin() - 1] + 50,
dp[lower_bound(a.begin()+1,a.begin()+1+i,a[i] - 1439) - a.begin() - 1] + 120});
}
rep(i, 1, n + 1)
cout<< dp[i] - dp[i - 1] << endl;
}

News About Credit

题意简述

一共有 \(n\) 名学生,第 \(i\) 个人的手机可以发送 \(a_i\) 条信息,但是他必须收到别人发给他的信息之后才可以发送给别人。

老师将信息首先告诉了第一个同学 \(a_1\) ,请问能否让每个同学都知道这条信息。

打印出发送信息的总数以及每次发送和接受的同学(确保所有人都知道情况下的任意一种方案即可)

思路分析

这是一个非常简单的模拟题,我们要让每个人都知道信息,实际上是一个知情者给不知情者发送信息的过程,最初的知情者为 \(a_1\) ,我们只需要从他开始模拟这个过程即可。

对于过程模拟,有一个贪心的小技巧,我们可以先将 \(a_2 ~ a_n\) 按照从大到小的顺序排序,然后每次都由当前信息最多的人发送给还不知情者。

对于这个过程,我们可以使用队列来优化,每次从队列中取出信息最多的同学(队头),然后发送给还不知道信息的同学,直到他拥有的信息花完,将他弹出,再取出新的队头,如此循环即可。

对于不知道信息的同学,我们可以采用一个指针 \(p\) 来维护,一开始只有 \(a_1\) 知情,因此 \(p\) 指向 \(a_2\),然后每次消耗队列中知情者的信息一次,就将 \(p\) 向右移动一次,同时,每次移动都将扫过的同学的可发送信息数 \(a_i (a_i > 0)\) 加入到队列中。过程中,如果 \(p\) 已经扫过了 \(n\) 位同学,则说明每个同学都已经知情,输出答案即可。如果出现 \(p < n\) 且队列元素为空,则说明可发送的信息总数已经用完,无法通知剩下的同学,输出 \(-1\) 即可。

复杂度分析

对每位同学可发送信息总数排序,时间复杂度为 \(O(n\log n)\)

\(p\) 指针一共移动 \(n\) 次,时间复杂度为 \(O(n)\)

总时间复杂度为 \(O(n\log n)\)

AC代码

void solve()
{
int n;
cin >> n;
vector<pii> a(n);
rep(i, 0, n) cin >> a[i].first, a[i].second = i + 1; //按可发送信息的数量排序,同时记录他原来的位置用于打印答案
queue<pii> q;
if (a[0].first) //将a1加入队列
q.push({1, a[0].first});
sort(a.begin() + 1, a.end(), [&](pii p1, pii p2) { return p1.first > p2.first; }); //对a2到an按照从大到小排序
int p = 1; //不知情者的指针,一开始指向第二个元素
vector<pii> t; //储存答案
while (q.size())
{
auto &top = q.front(); //由于我们每次发送信息要修改队头,因此采用引用
t.push_back({top.first, a[p].second}); //储存答案
if (a[p].first) //如果扫过的同学可发送信息数大于0,则加入队列
q.push({a[p].second, a[p].first});
p++, top.second--;
if (p >= n) //如果p扫过了n位同学,打印答案即可
{
cout << t.size() << endl;
for (auto [l, r] : t)
cout << l << " " << r << endl;
return;
}
if (!top.second) //如果当前队头可发送信息数已经用完,则把他弹出
q.pop();
}
cout << -1 << endl;
}

Beaver

题意简述

题目给出一个远串以及 \(n\) 个模式串,要求我们找出原串中不包含任何模式串的 最长子串

解题思路

首先,我们可以从原串中找出模式串,使用 KMP 算法可以帮助我们在 \(O(n)\) 的复杂度内完成对一个模式串的查询,利用 kmp 完成对所有的模式串的查询,保存所有查询结果 (匹配段)。

接着,我们可以用一个辅助数组来归类查询结果,数组的下标为模式串在原串中的开始下标,其对应的值为结束下标,对于有相同起点的区间,我们保留区间右端点最小的那一个,同时将不存在模式串区间起点的其他位置全部初始化为正无穷(在接下来会说明为什么要这么做)。

得到了存储有匹配段的数组后,我们可以从原串的末端点开始从后向前遍历,这里采用双指针技巧,一个指针 \(p\) 维护当前答案区间的右端点,一个指针 \(i\) 在辅助数组中向前扫描,一旦我们向前扫描的指针对应的数小于当前维护的答案右端点 (即 \(v_{i}\) < \(p\)),说明已经向前扫描的指针已经跨过了一个完整区间,我们需要将当前答案的右端点移动到所覆盖的模式串区间的右端点的左边第一位 (即 \(p = v_{i}-1\)),即可跳过当前的覆盖区间,同时,我们在指针移动的过程中不断更新答案,即可得到最终结果。

复杂度分析

我们每次对原串使用KMP求模式串,时间复杂度为 \(O(n)\),一共有 \(n\) 个模式串,因此总的时间复杂度为 \(O(nm)\)。

我们最后遍历匹配段,时间复杂度为 \(O(n)\),因此总的时间复杂度为 \(O(nm)\)。

AC代码

struct KMP //KMP模板
{
string pat;
vector<int> _next;
KMP(string _s)
{
pat = _s;
_next.resize(pat.size());
_next[0] = -1;
for (int i = 1, j = -1; i < pat.size(); i++)
{
while (~j && pat[i] != pat[j + 1])
j = _next[j];
if (pat[j + 1] == pat[i])
j++;
_next[i] = j;
}
}
vector<pii> query(string s)
{
vector<pii> p;
for (int i = 0, j = -1; i < s.size(); i++)
{
while (~j && s[i] != pat[j + 1])
j = _next[j];
if (s[i] == pat[j + 1])
j++;
if (j == pat.size() - 1)
{
p.push_back({i - j + 1, i - j + pat.size()});
j = _next[j];
}
}
return p;
}
}; void solve()
{
string s;
cin >> s;
int n;
cin >> n;
vector<string> pat(n);
rep(i, 0, n) cin >> pat[i];
vector<pii> skip; // 存储所有模式子串在原串中的起始下标
rep(i, 0, n) // 遍历模式串
{
KMP kmp(pat[i]);
auto temp = kmp.query(s); // 利用KMP得到原串中所有模式子串的起始下标并将其存入skip中
skip.insert(skip.end(), STED(temp));
}
vector<int> v(N, 1e9); // 初始化辅助数组
for (auto [l, r] : skip)
{
if (v[l] == 1e9)
v[l] = r;
else
v[l] = min(v[l], r); // 对于相同左端点的匹配段,仅保留右端点最小的那个
}
int p = s.size(), len = 0, pos = 0; // len和pos用来记录最长子串的起始位置和长度
for (int i = s.size(); i >= 1; i--)
{
if (v[i] <= p) // 如果向前的指针 i 已经跨过了一个匹配段的左端点,说明我们已经包含了一个匹配段
p = v[i] - 1; // 将答案右端点区间移动到当前匹配段右端点左边的第一个位置
if (p - i + 1 >= len) // 更新答案
{
len = p - i + 1;
pos = i - 1;
}
}
cout << len << " " << pos;
}

Magical Array

题意简述

如果一个数组的最小值和最大值相同,则称这个数组是Magical的。定义一个数组的子数组为一个数组中的连续的数组成的序列。给你一个数组,求这个数组中Magical的子数组有多少个。

思路分析

如果一个序列的最大值和最小值相同,那么这个序列一定所有元素都相同,因此题目实际上是要我们求相同的序列有多少个。

对于长度为 \(n\) 的相同序列,他可以排列出如下子序列:

\[\begin{gather}
a_1,a_2,a_3,\cdots,a_{n-2},a_{n-1},a_n\nonumber\\
a_2,a_3,a_4,\cdots,a_{n-2},a_{n-1},a_n\nonumber\\
a_3,a_4,a_5,\cdots,a_{n-2},a_{n-1},a_n\nonumber\\
\cdots\nonumber\\
a_{n-2},a_{n-1},a_n\nonumber\\
a_{n-1},a_n\nonumber\\
a_n\nonumber\\
\end{gather}
\]

实际上总数即为

\[\sum_{i=1}^n = \frac{n(n+1)}{2}
\]

所以,我们只需要计算每个连续且相同的子串的长度经过计算,即可得到答案。

复杂度分析

遍历一遍序列\(O(n)\)

每次处理子串可以直接计算,也可以预处理,\(O(1)\)

总时间复杂度\(O(n)\)

AC代码

void solve()
{
vector<int> pre(N);
for(int i=1;i<=1e5;i++) pre[i] = pre[i-1] + i; //预处理累和
int n;
cin >> n;
vector<int> a(n);
rep(i,0,n) cin >> a[i];
int ans = 0,len = 1;
rep(i,1,n)
{
if(a[i] != a[i-1]) //如果与前面不同,则说明上一个相同连续子串在此结束
{
ans += pre[len]; //累加答案
len = 1;
}
else len++; //相同连续子串长度+1
}
ans += pre[len]; //处理位于末尾的连续相同子串
cout << ans;
}

Strange Game On Matrix

题意简述

给你一个仅包含\(1\) 和 \(0\) 的大小为 \(n\times m\) 的矩阵和 \(k\),你可以选择一个该列的任意一个 \(1\),从这个 \(1\) 向下出发(包含),直到长度为 \(\min(k,n-i+1)\)。其中的 \(1\) 的个数加到总分数中,同时,在你选择的 \(1\)上方的所有的 \(1\)将被计入总代价。

每一列找完之后,得到总分数。求保证总分数最大的情况下,代价最小是多少?输出最大总分数和代价。

解题思路

题目给出的数据量并不大,因此我们可以直接对每一列进行长度为 \(k\)的区间枚举(同样类似于滑动窗口)。

对于区间枚举,我们可以采用前缀和的技巧,预处理出每一列的前缀和。在进行区间询问的的时候,我们的总分数即为 \(pre[i+k]-pre[i-1]\),而总代价即为\(pre[i-1]\),每次更新答案即可

复杂度分析

枚举每一列的 \(k\) 个长度区间,\(O(n)\)

枚举每一列,复杂度为\(O(m)\)

总复杂度为\(O(nm)\)

AC代码

void solve()
{
int n, m, k;
cin >> n >> m >> k;
vector<vector<int>> g(n + 1, vector<int>(m + 1)), pre(n + 1, vector<int>(m + 1));
rep(i, 1, n + 1)
rep(j, 1, m + 1)
cin >> g[i][j]; //读入矩阵
rep(j, 1, m + 1)
rep(i, 1, n + 1)
{
pre[i][j] = pre[i - 1][j] + g[i][j]; //处理每一列的前缀和
}
int ans = 0, cnt = 0;
rep(j, 1, m + 1)
{
int p = 0, t = 0;
rep(i, 1, n + 2 - k) //枚举每一列长度为k的区间
{
int sum = pre[i + k - 1][j] - pre[i - 1][j]; //区间中1的数量
//如果当前区间的分数更高,或者分数相同代价更小,则更新答案
if (sum > p || (sum == p && pre[i - 1][j] < t))
{
p = sum, t = pre[i - 1][j];
}
}
ans += p, cnt += t;
}
cout << ans << " " << cnt << endl;
}

XK Segments

题意简述

给定一个长度为 \(n\) 的数组 \(a\),我们定义一对合法二元组 \((i,j)\),满足 \(a_j\) > \(a_i\),且\(a_j ~ a_i\) 之间刚好存在 \(k\) 个 \(x\) 的倍数(包括端点)。

求出所有合法二元组的数量

tips:若 \(i \neq j\), \((i,j)\) 和 \((j,i)\) 视为不同二元组

思路解析

因为 \((i,j)\) 和 \((j,i)\) 视为不同二元组,因此,我们很容易发现,我们要求的二元组对顺序并没有要求,因此我们可以先对 \(a\) 进行排序。

对于每一个 \(a_i\),我们要求有哪些 \(a_j\),可以与其构成合法二元组,实际上是求序列内,有多少个数 \(a_j\) 满足 $ k \times x \geq a_j - a_i \geq (k-1) \times x$,只有满足条件的 \(a_j\) 才能构成合法二元组。因此,我们可以采用二分查找符合范围的区间左右边界,然后计算区间内合法二元组的数量即可。

特别的,由于端点也计算在 \(k\) 个 \(x\) 内,因此我们需要做出一些特判边界的情况, 如果 \(a_i\) 本身是 \(x\) 的倍数,那么我们的起点可以直接记为 \(a_i\) ,否则,起点需要从大于 \(a_i\) 的第一个 \(x\) 的倍数开始计算,即 \(a_i + (x - a_i \bmod x)\),综合两种情况,我们可以总结成以下公式:

\[\begin{gather}
起点 st = a_i + (x - a_i \bmod x) \bmod x\\
左边界 L = st + x \times (k - 1)\\
右边界 R = st + x \times k\\
\end{gather}
\]

还有一种特殊情况,如果 \(k = 0\) 时,我们直接让左端点等于 \(a_i\) 即可。

复杂度分析

我们先对序列 \(a\) 排序,时间复杂度为 \(O(n \log n)\)。

我们枚举每个 \(a_i\) ,时间复杂度为 \(O(n)\)。

对于每个枚举的 \(a_i\),我们采用二分查找合法区间的边界,时间复杂度为 \(O(\log n)\)。

因此,总的时间复杂度为 \(O(n \log n)\)。

AC代码

void solve()
{
int n, x, k;
cin >> n >> x >> k;
vector<int> a(n);
rep(i, 0, n) cin >> a[i];
sort(STED(a));
int ans = 0;
rep(i, 0, n)
{
int st = a[i] + (x - a[i] % x) % x; //起点
int l = st + x * (k - 1), r = st + x * k; //左右边界
if (k == 0) //特判k=0
l = a[i];
ans += lower_bound(STED(a), r) - lower_bound(STED(a), l);//中间的数就是合法的数字,累计
}
cout << ans << endl;
}

Seat Arrangements

题意简述

给你一个 \(n \times m\) 的矩阵,仅包含 \(*\) 和 \(.\) 两种字符,其中 \(.\) 表示可选位置,\(*\) 表示不可选位置,你需要在可选的位置中找到水平连续或者竖直连续的 \(k\) 个位置。问你有多少种选择位置的方案。

解题思路

事实上,对于一列连续的空座位,如果其数量 \(n\) 大于 \(k\),那么他一定产生 \(n - k + 1\) 种选法。

以下是 \(k = 3\),\(n = 5\) 的例子:

\[\begin{gather}
|a_1,a_2,a_3|,a_4,a_5\nonumber\\
a_1,|a_2,a_3,a_4|,a_5\nonumber\\
a_1,a_2,|a_3,a_4,a_5|\nonumber\\
\end{gather}
\]

因此,我们只需要遍历一次每一行中连续的空位置个数,然后与 \(k\),比较,如果大于 \(k\),那么就加上 \(n - k + 1\) 的方案数。

然后再使用相同的方法遍历每一列,最后将两者的方案数相加即可。

!tip:特别的,如果 \(k = 1\) 的话,我们在水平遍历和垂直遍历的时候会将一个空位累加2次,因此我们需要做出一个特判,如果 \(k = 1\) 的话,那么答案应该除以二。

复杂度分析

我们遍历了每一行和每一列,时间复杂度为 \(O(n \times m)\)

AC代码

void solve()
{
int n, m, k, ans;
char G[N][N];
cin >> n >> m >> k;
rep(i, 0, n) cin >> G[i]; //可以使用字符串的方式直接读入矩阵
rep(i, 0, n) //遍历行
{
int cnt = 0;
rep(j, 0, m)
{
if (G[i][j] == '.')
cnt++;
if (G[i][j] == '*' || j == m - 1) //如果找到一个*或者已经到本行末尾的话
{
if (cnt >= k) //如果连续的空位大于 k,则累加答案
ans += cnt - k + 1;
cnt = 0;
}
}
}
rep(j, 0, m) //遍历列
{
int cnt = 0;
rep(i, 0, n)
{
if (G[i][j] == '.')
cnt++;
if (G[i][j] == '*' || i == n - 1)
{
if (cnt >= k)
ans += cnt - k + 1;
cnt = 0;
}
}
}
if (k == 1)
ans /= 2; //特判 k = 1
cout << ans << endl;
}

ALIEN - Aliens at the train

题意简述

给你一个长度为 \(n\),的序列,问你序列中总和不超过 \(x\) 的最长子串中,总和最小的一个子串的长度和他的总和是多少。

先满足最长,再满足总和最小

解题思路

这是一个典型的双指针问题,如果采用暴力做法的话,我们以每个数作为起点,然后遍历后面的数,直到总和大于 \(x\) 为止,然后更新答案。这样做的时间复杂度是 \(O(n^2)\) 的,一定会超时。

我们可以采用类俗于 Music in Car 的思路来解决这个问题,首先,我们要快速询问区间和,前缀和这一技巧可以满足我们的需要,然后我们用一个指针 \(r\) 维护区间右端点,然后向后遍历,每次向后移动一次,就将对应的数加入我们目前的总和当中,如果出现了总和大于 \(x\) 的情况,我们就可以向右移动另一个指针 \(l\) ,每次移动 \(l\),就将其扫过的数删去,直到总和不大于 \(x\) 为止,然后更新答案。

复杂度分析

对于 \(r\) 指针和 \(l\) 指针,他们扫过的区间都是整个序列,所以时间复杂度是 \(O(n)\) 的。

AC代码

void solve()
{
int n, x;
cin >> n >> x;
vector<int> a(n + 1), pre(n + 1);
rep(i, 1, n + 1) cin >> a[i], pre[i] = pre[i - 1] + a[i]; //求出前缀和
int l = 0;
int ans = 0, len = 0;
for (int r = 1; r <= n; r++) //r指针向右扫描
{
while (pre[r] - pre[l] > x) //如果区间总和大于x,则移动l指针
l++;
if (r - l > len) //如果找到一个更长的区间,更新答案
len = r - l, ans = pre[r] - pre[l];
if (r - l == len) //如果找到一个等长的区间但是其总和更小,更新答案
ans = min(ans, pre[r] - pre[l]);
}
cout << ans << " " << len << endl;
}

ARRAYSUB - subarrays

题意简述

滑动窗口(单调队列)的模板题

关于单调队列

由于我们要求的是区间的最大数,我们可以用一个双端队列来维护整个区间,队列中存储的是数组下标,而且队列中的元素保证单调递减,对于一个区间来说,我们每一次只会取区间中最大的元素,也就对应队列中的队头元素了

关于如何维护队列,我们每次向前遍历一个数,首先需要检查队头元素的下标是否与我们当前遍历到的下标相差了一个区间长度 \(k\),如果已经超过了,说明他已经不在窗口中了,应该弹出,其次,对于每一个新加入的元素,我们依次将他与队列中的元素比较,如果比队尾元素大,那么就弹出队尾元素,直到队尾元素大于等于新加入的元素为止,这样我们就维护了一个单调递减的队列,队头元素就是当前窗口中的最大值,每次输出即可。

复杂度分析

遍历一边整个数组,复杂度 \(O(n)\)

AC代码

#include <iostream>
#include <deque>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n, m;
int nums[N];
deque<int> q;
int main()
{
cin >> n;
for (int i = 0; i < n; i++)
cin >> nums[i];
cin >> m;
for (int i = 0; i < n; i++)
{
if (q.size() && i - q.front() >= m) //如果队头已经滑出窗口,弹出
q.pop_front();
while (q.size() && nums[i] >= nums[q.back()]) //将新元素依次与队尾比较,如果队尾小于新加入的元素,则弹出
q.pop_back();
q.push_back(i); //加入新元素
if (i >= m - 1)
cout << nums[q.front()] << " "; //每次队头就是区间中的最大值
}
return 0;
}

CEIT算法训练-双指针部分题解(全12题)的更多相关文章

  1. 算法训练 Lift and Throw

    算法训练 Lift and Throw   时间限制:3.0s   内存限制:256.0MB      问题描述 给定一条标有整点(1, 2, 3, ...)的射线. 定义两个点之间的距离为其下标之差 ...

  2. 蓝桥杯 算法训练 ALGO-116 最大的算式

    算法训练 最大的算式   时间限制:1.0s   内存限制:256.0MB 问题描述 题目很简单,给出N个数字,不改变它们的相对位置,在中间加入K个乘号和N-K-1个加号,(括号随便加)使最终结果尽量 ...

  3. 算法笔记_067:蓝桥杯练习 算法训练 安慰奶牛(Java)

    目录 1 问题描述 2 解决方案   1 问题描述 问题描述 Farmer John变得非常懒,他不想再继续维护供奶牛之间供通行的道路.道路被用来连接N个牧场,牧场被连续地编号为1到N.每一个牧场都是 ...

  4. 蓝桥杯 算法训练 ALGO-21 装箱问题

     算法训练 装箱问题   时间限制:1.0s   内存限制:256.0MB 问题描述 有一个箱子容量为V(正整数,0<=V<=20000),同时有n个物品(0<n<=30),每 ...

  5. 蓝桥杯 算法训练 ALGO-149 5-2求指数

     算法训练 5-2求指数   时间限制:1.0s   内存限制:256.0MB 问题描述 已知n和m,打印n^1,n^2,...,n^m.要求用静态变量实现.n^m表示n的m次方.已知n和m,打印n^ ...

  6. 蓝桥杯 算法训练 ALGO-156 表达式计算

    算法训练 表达式计算   时间限制:1.0s   内存限制:256.0MB 问题描述 输入一个只包含加减乖除和括号的合法表达式,求表达式的值.其中除表示整除. 输入格式 输入一行,包含一个表达式. 输 ...

  7. 蓝桥杯 算法训练 ALGO-125 王、后传说

    算法训练 王.后传说   时间限制:1.0s   内存限制:256.0MB 问题描述 地球人都知道,在国际象棋中,后如同太阳,光芒四射,威风八面,它能控制横.坚.斜线位置. 看过清宫戏的中国人都知道, ...

  8. 蓝桥杯 算法训练 ALGO-118 连续正整数的和

    算法训练 连续正整数的和   时间限制:1.0s   内存限制:256.0MB 问题描述 78这个数可以表示为连续正整数的和,1+2+3,18+19+20+21,25+26+27. 输入一个正整数 n ...

  9. 蓝桥杯 算法训练 ALGO-140 P1101

    算法训练 P1101 时间限制:1.0s 内存限制:256.0MB    有一份提货单,其数据项目有:商品名(MC).单价(DJ).数量(SL).定义一个结构体prut,其成员是上面的三项数据.在主函 ...

  10. 蓝桥杯 算法训练 最短路 [ 最短路 bellman ]

    传送门   算法训练 最短路   时间限制:1.0s   内存限制:256.0MB     锦囊1   锦囊2   锦囊3   问题描述 给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证 ...

随机推荐

  1. CF1800E 题解

    发现一个神奇的事实:显然不限制交换次数可以实现交换任意字符. 因此可以直接判断字符集是否相等. 在考虑哪些地方可以交换. 根据题意可知可以交换的区间为 \([1,n - k]\) 以及 \([k + ...

  2. [oeasy]python0028_直接运行_修改py文件执行权限_设置py文件打开方式

    ​ 直接运行 回忆上次内容 我们把两个程序整合起来了 可以持续输出当前时间 每秒都更新 ​ 编辑 但是我想在 shell 里面 只输入文件名(./sleep.py)并回车 就能不断输出时间 可能吗? ...

  3. Python 使用rsa类库基于RSA256算法生成JWT

    JWT简介 JWT(Json web token),是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准.JWT提供了一种简单.安全的身份认证方法,特别适合分布式站点单点登录.或者是签名. ...

  4. GUI随笔

    ####GUI是一个很大的话题,从Win32(windows基础API编程)到MFC,QT再到DuiLib,WPF,Winform再到Html这是一个很漫长的路,下面是我对这个界面库的见解 就对我而言 ...

  5. 单细胞测序最好的教程(十):细胞类型注释迁移|万能的Transformer

    作者按 本章节主要讲解了基于transformer的迁移注释方法TOSICA,该算法在迁移注释上达到了SOTA的水平,在注释这么卷的赛道愣是杀出了一条血路.本教程首发于单细胞最好的中文教程,未经授权许 ...

  6. Jmeter函数助手1-CSVRead

    CSVRead函数适用于读取文件获取参数值. 用于获取值的CSV文件 | *别名:csv文件路径 CSV文件列号| next| *alias:读取列,0表示第一列,1表示第二列 1.首先我们需要一个文 ...

  7. 9、IDEA集成Github

    9.1.登录Github账号 9.1.1.打开IDEA的Settings界面 如上图所示,打开IDEA的 Settings(设置)界面. 9.1.2.使用账号密码登录(方式一) 如上图所示,在&quo ...

  8. Tomcat日志信息有乱码的处理方法

    1.问题描述 1.1.Idea中的tomcat日志有乱码 1.2.直接启动tomcat的日志有乱码 1.3.原因分析 问题是由于tomcat使用的编码和操作系统使用的编码不一致导致: Windows1 ...

  9. 【C】Re04

    一.类型限定符 extern 声明一个变量,extern声明的变量没有存储空间 const 定义一个常量,该常量必须赋值,之后且不允许更改 volatile 防止编译器优化代码??? register ...

  10. 【转载】冲压过程仿真模拟及优化 —— 冲压仿真的方法分类PPT

    地址: https://www.renrendoc.com/paper/310415051.html