CF Round 767 Div2 题解
A题 Download More RAM
总计 \(T\) 组数据(\(1\leq T \leq 100\))。
现在我们有大小为 \(k\) 的内存,外加 \(n\) 个扩容器:第 \(i\) 个扩容器在载入的时候需要消耗 \(a_i\) 的内存(载入完毕之后就不消耗了),然后永久给内存容量增加 \(b_i\)。
问我们至多可以将内存扩大到多少?
\(1\leq n\leq 100,1\leq k,a_i,b_i\leq 10^3\)
显然,我们将扩容器按照 \(a\) 的大小排序,优先用小的即可。
#include<bits/stdc++.h>
using namespace std;
const int N = 110;
int n, k;
struct Node {
int a, b;
bool operator < (const Node &rhs) const {
return a < rhs.a;
}
} t[N];
void solve()
{
//read
cin >> n >> k;
for (int i = 1; i <= n; ++i)
cin >> t[i].a;
for (int i = 1; i <= n; ++i)
cin >> t[i].b;
//solve
sort(t + 1, t + n + 1);
for (int i = 1; i <= n; ++i) {
if (t[i].a <= k) k += t[i].b;
else break;
}
cout << k << endl;
}
int main()
{
int T;
cin >> T;
while (T--) solve();
return 0;
}
B题 GCD Arrays
有 \(T\) 组数据(\(1\leq T \leq 10^5\))。
给定 \(a,l,r\),则 \(\{a_n\}\) 为一个 \([l,r]\) 上面的连续数列(例如 \(l=3,r=7\),则 \(\{a_n\}=\{3,4,5,6,7\}\))。
我们可以进行一种操作:从数列中选择任意两个数,然后从数列中删除他俩,随后向数列中插入这两个数的乘积。问,我们能否在进行不超过 \(k\) 次操作的情况下,使得 \(\operatorname{gcd}(a)>1\)?
\(1\leq l\leq r\leq 10^9,0\leq k \leq r-l\)
序列长度为 1 的时候,看看这个唯一的数是不是 1 就行了。
序列长度大于 1 时,那就是看整个数列缩到最后之后,每个数都有一个大于 1 的质因数。因为初始数列连续,那么不难想到这个质因数为 2(让这个质因数为 3 或者更大的数,难度要比 2 大得多)。
如果序列长度为偶数,那么奇偶一一配对即可,反之则加一减一来调一调(对于长度为 2 或者 3 的数列似乎也适用)。
#include<bits/stdc++.h>
using namespace std;
bool solve()
{
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
if (k == 0) return l == r && l != 1;
int len = r - l + 1;
if (len % 2 == 0) return len / 2 <= k;
else {
if (l % 2 == 0) return len / 2 <= k;
else return len / 2 + 1 <= k;
}
}
int main()
{
int T;
cin >> T;
while (T--) puts(solve() ? "YES" : "NO");
return 0;
}
C题 Meximum Array
总计 \(T\) 组数据(\(1\leq T \leq 100\))。
对于非负整数序列 \(\{a_n\}\),记 \(\operatorname{mex}(a)\) 为最小的且未在 \(a\) 中出现的非负整数。
现在给定一个数列 \(\{a_n\}\),问我们能否将其划为几段,使得各段的 \(\text{mex}\) 组成的新数列的字典序最大(并输出它)?(遵循字符串的字典序定义)
\(\sum n\leq 2*10^5,0\leq a_i\leq n\)
抛开算法不谈,我们先看看怎么构造这玩意。
我们假设我们已经处理好了前 \(L-1\) 个数,现在准备处理 \([L,n]\) 上面的数。
- 显然,我们要处理下,看看这部分的 \(\text{mex}\) 是多少。
- 随后,我们压缩右区间 \(R\),即找出最小的 \(R\),使得 \([L,R]\) 上面的 \(\text{mex}\) 和 \([L,n]\) 相同。
- 让 \(L=R+1\),继续循环,直到 \(L=n+1\) 为止。
但是很离谱的是,每一轮的复杂度都能够达到最坏 \(O(n)\),而整个程序最坏能进行 \(n\) 轮,即总复杂度 \(O(n^2)\),这显然无法接受,必须降到 \(O(n\log n)\) 或者 \(O(n)\)。
对于处理 \(\text{mex}\),我们有这样一种策略:我们首先预处理出 \([1,n]\) 上面的 \(\text{mex}\)(开一个 \(vis\) 数组(计数用的,并非纯粹判断这个数是否填过),填完之后从 0 到 n 扫一遍)。每次移动左端点 \(L\) 时,我们依次清理在 \(vis\) 里面的记录,如果过程中发现 \(vis_x=0\) 且 \(x < \text{Mex}\),那么我们将 \(\text{Mex}\) 更新为 \(x\)。
可惜的是,我们只完成了左端点的优化,对于右端点的移动没啥方法,最坏复杂度依然为 \(O(n^2)\)。
法一:树状数组维护区间和
我们尝试更改一下套路,让右端点的移动从左向右进行,这样就实现了双指针,框架复杂度可以达到 \(O(n)\),里面还可以掺一些 \(O(\log n)\) 的操作。从正向角度,我们不难想到一种策略:我们再开一个 \(vis\) 数组(这里不计数,纯粹用 01 表示有无),随后使 \(R\) 从 \(L\) 开始,逐渐推进,当 \(\sum\limits_{i=0}^{x-1}vis_i=x\) 且 \(vis_x=0\) 时,则 \([L,R]\) 上面的 \(\text{mex}\) 就是 \(x\)。又要修改又要查询,那真就只有树状数组和线段树了(注意,这两个数据结构正常都是维护 \([1,n]\) 上面的,所以对于题目中那种需要维护 \([0,n]\) 的,需要手动维护下标为 0 的部分或者整体平移区间,还有就是这个 \(vis\) 不是随便加的,如果本来为 1 就别继续加,本来为 0 就别继续减啥的,反正小细节亿堆)。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N];
int vis[N];
int Mex;
//
struct TreeArray {
int arr[N];
inline int lowbit(int x) { return x & (-x); }
int ask(int x) {
x++;
int res = 0;
for (; x; x -= lowbit(x)) res += arr[x];
return res;
}
int query(int l, int r) {
return ask(r) - ask(l - 1);
}
int getv(int x) {
return query(x, x);
}
void add(int x, int val) {
if ((getv(x) == 1 && val == 1) || (getv(x) == 0 && val == -1)) return;
x++;
for (; x <= n + 1; x += lowbit(x))
arr[x] += val;
}
} ta;
//
int getR(int L, int mex) {
for (int i = L; i <= n; ++i) {
ta.add(a[i], 1);
if (ta.query(0, mex - 1) == mex && ta.getv(mex) == 0)
return i;
}
return n;
}
void clear(int L, int R) {
for (int i = L; i <= R; ++i) {
vis[a[i]]--;
ta.add(a[i], -1);
if (vis[a[i]] == 0 && a[i] <= Mex)
Mex = a[i];
}
}
int ans[N];
void solve()
{
//read
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//init
memset(vis, 0, sizeof(int) * (n + 1));
memset(ta.arr, 0, sizeof(int) * (n + 2));
for (int i = 1; i <= n; ++i)
vis[a[i]]++;
for (int i = 0; i <= n; ++i)
if (vis[i] == 0) {
Mex = i;
break;
}
//two pointer
int L = 1, cnt = 0;
while (L <= n) {
int R = getR(L, Mex);
ans[++cnt] = Mex;
clear(L, R);
L = R + 1;
}
//output
printf("%d\n", cnt);
for (int i = 1; i <= cnt; ++i)
printf("%d ", ans[i]);
puts("");
}
int main()
{
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}
法二:set 维护
为了一个简单的需求来写一个树状数组太逆天了(昨天还给我调了将近半小时,最离谱的是还没调出来),所以我们尝试一下别的更简单的数据结构。我们综合一下,分析一下需求,发现我们需要:
- 对于一个 bool 数列求和(值仅有 01,且查询的区间固定)
- 能够快速的进行 bool 加
- 用完之后可以光速清零
我们不妨在开桶记录的时候,只有小于 \(\text{Mex}\) 的才统计,然后单独开一个 \(cnt\) 变量来判断是否 \(\sum\limits_{i=0}^{x-1}vis_i=x\)(考虑到题目保证有解,那么一些极端情况就不考虑了)。不过普通的基于数组的桶,清零是基于值域的,比较麻烦,所以我们用 set 或者 map 进行代替。
#include <bits/stdc++.h>
using namespace std;
const int N = 200010;
int n, a[N], vis[N], Mex;
//
int getR(int L, int mex) {
set<int> s;
int cnt = 0;
for (int i = L; i <= n; ++i) {
if (a[i] < Mex) s.insert(a[i]);
if (s.size() == Mex) return i;
}
}
void clear(int L, int R) {
for (int i = L; i <= R; ++i) {
vis[a[i]]--;
if (vis[a[i]] == 0 && a[i] <= Mex)
Mex = a[i];
}
}
int ans[N];
void solve()
{
//read
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
//init
memset(vis, 0, sizeof(int) * (n + 1));
for (int i = 1; i <= n; ++i)
vis[a[i]]++;
for (int i = 0; i <= n; ++i)
if (vis[i] == 0) {
Mex = i;
break;
}
//two pointer
int L = 1, cnt = 0;
while (L <= n) {
int R = getR(L, Mex);
ans[++cnt] = Mex;
clear(L, R);
L = R + 1;
}
//output
printf("%d\n", cnt);
for (int i = 1; i <= cnt; ++i) printf("%d ", ans[i]);
puts("");
}
int main()
{
int T;
scanf("%d", &T);
while (T--) solve();
return 0;
}
D题 Peculiar Movie Preferences
有 \(T\) 组数据(\(1\leq T \leq 100\))。
给定 \(n\) 个长度至多为 3 的字符串,问能否从中选几个重新排列,以组成一个新的回文串?
\(\sum n \leq 10^5\)
本题有一个不是很显然,不好证明,但是多玩几组样例之后会隐约发现的结论:如果能的话,只需要至多两个字符串即可凑成一个新的回文串。
证明的话,详情可见官方题解,我这里简单说两个感性想到的点:
- 如果有长度为 1 的串,那么自己就是回文串,也就不需要拼,所以需要超过 2 个字符串凑成的回文串,子串必然都是长度为 2 或 3
- 对于这个长回文串,显然仅保留头尾两个字符串,同样是一个回文串(22,23,32,33四种情况均成立)
如果仅需要一个字符串就可以拼成,那么我们直接对 \(n\) 个字符串一一扫一遍即可。
如果需要两个,分类讨论:
- 22 或者 33 型,看看自己的反转串有没有出现过(开个 map 就行)
- 23 或者 32 型,把那个长度为 2 的反转过来,然后看看是不是谁的前缀或者后缀
注意,这玩意必须按照原顺序组成回文串,所以 23 和 32 在遍历时候必须按照一定顺序(顺序或者倒序),详情看代码。
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int n;
string str[N];
string Rev(string s) {
reverse(s.begin(), s.end());
return s;
}
bool solve()
{
//read
cin >> n;
for (int i = 1; i <= n; ++i)
cin >> str[i];
//solve
//type1
for (int i = 1; i <= n; ++i)
if (str[i] == Rev(str[i])) return true;
//type2
map<string, int> vis1;
for (int i = 1; i <= n; ++i) vis1[str[i]] = 1;
for (int i = 1; i <= n; ++i)
if (vis1.find(Rev(str[i])) != vis1.end()) return true;
//type3
map<string, int> vis2;
for (int i = n; i >= 1; --i) {
if (str[i].length() == 3) vis2[str[i].substr(1, 2)] = 1;
else if (vis2.find(Rev(str[i])) != vis2.end()) return true;
}
//type4
map<string, int> vis3;
for (int i = 1; i <= n; ++i)
if (str[i].length() == 3) vis3[str[i].substr(0, 2)] = 1;
else if (vis3.find(Rev(str[i])) != vis3.end()) return true;
return false;
}
int main()
{
ios::sync_with_stdio(false);
int T;
cin >> T;
while (T--)
cout << (solve() ? "YES" : "NO") << endl;
return 0;
}