AtCoder Beginner Contest 403 ABCDEFG 题目解析
A - Odd Position Sum
题意
给定一个长度为 \(N\) 的数组,求出所有奇数位置元素之和。
代码
#include<bits/stdc++.h>
using namespace std;
int main()
{
int n;
cin >> n;
int sum = 0;
for(int i = 1; i <= n; i++)
{
int d;
cin >> d;
if(i % 2 == 1)
sum += d;
}
cout << sum;
return 0;
}
B - Four Hidden
题意
给定一个仅包含小写英文字母以及 ?
的字符串 \(T\),以及一个仅包含小写英文字母的字符串 \(U\)。
字符串 \(T\) 中的 ?
可以被替换为任意一个小写英文字母。
问是否存在某种替换方案,使得 \(U\) 是 \(T\) 的一个子串?
思路
由于数据范围很小,直接暴力比较即可。
每次枚举 \(T\) 的某个位置作为起点,每次比较两字符,要么相等,要么其中一个是 ?
即可。
代码
#include<bits/stdc++.h>
using namespace std;
char s[15], t[15];
int main()
{
cin >> s >> t;
int n = strlen(s);
int m = strlen(t);
for(int i = 0; i + m <= n; i++) // 枚举 s[i] 作为此次比较的起点
{
bool flag = true;
for(int j = 0; j < m; j++)
{
if(s[i + j] != '?' && s[i + j] != t[j]) // 只要有一个位置不对应
{
flag = false;
break;
}
}
if(flag)
{
cout << "Yes";
return 0;
}
}
cout << "No";
return 0;
}
C - 403 Forbidden
题意
有 \(N\) 个用户以及 \(M\) 个网页。一开始每个用户都不能访问任何一个网页。
有 \(Q\) 次询问,每次询问是以下三种之一:
1 X Y
:表示授予用户 \(X\) 访问网站 \(Y\) 的权限。2 X
:表示授予用户 \(X\) 访问所有网站的权限。3 X Y
:询问用户 \(X\) 是否可以访问网站 \(Y\)。
思路
首先可以直接用一个计数数组来判断某个用户是否已经被授予访问所有网站的权限了。
如果没有,可以使用 set
或 map
等关系型容器来存储每个用户分别有哪些网站的权限,来实现快速的增加与查询操作。
代码
#include<bits/stdc++.h>
using namespace std;
set<int> st[200005]; // st[x] 存用户 x 可以访问的所有网站
bool all[200005]; // all[x] 表示用户 x 是否可以访问所有网站
int main()
{
int n, m, q;
cin >> n >> m >> q;
while(q--)
{
int op, x, y;
cin >> op;
if(op == 1)
{
cin >> x >> y;
st[x].insert(y);
}
else if(op == 2)
{
cin >> x;
all[x] = true;
}
else
{
cin >> x >> y;
// 要么已经有全部权限,要么 st[x] 内存在访问网站 y 的权限
if(all[x] || st[x].find(y) != st[x].end())
cout << "Yes\n";
else
cout << "No\n";
}
}
return 0;
}
D - Forbidden Difference
题意
给定一个长度为 \(N\) 的数组以及一个非负整数 \(D\)。
问至少删除多少个数字,才能够使得数组内不存在任何两个差值为 \(D\) 的元素。
思路
首先本题与数组中每个数字出现的位置无关,且数字范围在 \(0 \sim 10^6\) 范围内,因此可以先采用计数数组来统计每种数字出现的次数。记 cnt[x]
表示 \(x\) 出现的次数。
首先,如果 \(D = 0\),表示每种数字只能保留一个,直接处理即可。
否则,只有当存在整数 \(x\) 满足 \(cnt_x \gt 0\) 并且 \(cnt_{x + D} \gt 0\) 时,我们才需要对 \(x\) 和 \(x+D\) 其中的某个数字进行删除,或是都删除。
- 什么情况下需要将相邻两种差值为 \(D\) 的整数都删除呢?考虑 \(cnt_x, cnt_{x+D}, cnt_{x+2D}, cnt_{x+3D} \gt 0\),假设四个值分别为 \(\{2,1,1,2\}\),明显我们删除 \(x+D\) 和 \(x+2D\) 这两种数字是最优秀的,只需要删除 \(1+1=2\) 个。
可以明确,最终我们需要满足的目标是:对于任何一个整数 \(x\),要么 \(cnt_x=0\),要么 \(cnt_{x+D} = 0\)。
为了达成以上目标,对于整数 \(x\),我们可以考虑由 \(cnt_x, cnt_{x+D}, cnt_{x+2D}, \dots, cnt_{x+yD}\) 所组成的序列(其中 \(y\) 是满足 \(x + yD \le 1000000\) 的最大整数)。我们需要将这个序列的某些位置改为 \(0\),使得最终不存在任意两个非零的数字相邻,并且要使得剩余的数字总和最大。
考虑动态规划,每次从上述序列当中取出一段连续的非零子序列,记作 \(\{v_1, v_2, v_3, \dots, v_M\}\)。我们的任务是取出序列中的某些数字,在保证取的数字不相邻的前提下,使其总和最大,以表示剩余数字数量的最大值。
定义 \(dp[i][0/1]\) 表示考虑从前往后考虑到 \(v_i\) 过,且 \(j=0\) 表示 \(v_i\) 不取,\(j=1\) 表示 \(v_i\) 要取的情况下所能得到的最大总和。
容易得出状态转移方程:
最后剩余的所有数字总和最大值即 \(\max(dp[M][0], dp[M][1])\)。
题目要求的是最少删除多少个数字,因此我们可以记原本的 \(v\) 数组总和为 \(sum\) (也就是删除之前总共有多少个数字),答案即 \(sum - \max(dp[M][0], dp[M][1])\)。
明显我们只需要对于 \(x \in [0, D-1]\) 中的每个 \(x\),都按照上述方式处理一遍,就可以考虑完所有 \(0 \sim 1000000\) 以内的所有数字,最后取总和即为最终解。
代码
#include<bits/stdc++.h>
using namespace std;
int n, d;
int cnt[1000005];
int dp[1000005][2];
// 对 vec 数组进行 dp,要求取作答案的数字不相邻
int solve(vector<int> &vec)
{
if(vec.empty())
return 0;
int sum = 0; // 存 vector 总和
// 由于 vector 下标从 0 开始,这里采用从 i 转移到 i+1 的写法
for(int i = 0; i < vec.size(); i++)
{
dp[i + 1][0] = max(dp[i][0], dp[i][1]);
dp[i + 1][1] = dp[i][0] + vec[i];
sum += vec[i];
}
return sum - max(dp[vec.size()][0], dp[vec.size()][1]);
}
int main()
{
cin >> n >> d;
for(int i = 1; i <= n; i++)
{
int x;
cin >> x;
cnt[x]++;
}
int ans = 0;
if(d == 0)
{
for(int i = 0; i <= 1000000; i++)
ans += max(0, cnt[i] - 1); // 每种数字最多保留一个
}
else
{
for(int i = 0; i < d; i++)
{
vector<int> vec;
// 从 i 开始,取 i, i+d, i+2d, i+3d, ... 所有数字
for(int j = i; j <= 1000000; j += d)
{
if(cnt[j] > 0) // 非 0,加入数组
vec.push_back(cnt[j]);
else // 遇到 0,则将前面这段非 0 序列做一次 dp 后清空即可
{
ans += solve(vec);
vec.clear();
}
}
ans += solve(vec); // 不要忘了最后这段非 0 序列
}
}
cout << ans;
return 0;
}
E - Forbidden Prefix
题意
一开始有两个空的多重集合 \(X\) 和 \(Y\)。
有 \(Q\) 次操作,操作按顺序进行,第 \(i\) 次操作会给定一个整数 \(T_i\) 和一个字符串 \(S_i\):
- 如果 \(T_i = 1\),将 \(S_i\) 加入到集合 \(X\) 内
- 如果 \(T_i = 2\),将 \(S_i\) 加入到集合 \(Y\) 内
在每次操作结束后,请输出一个整数,表示:
- \(Y\) 内总共有多少个字符串,满足 \(X\) 中不存在任意一个字符串是它的前缀。
思路
假设现在有一个字符串 \(S\) 需要加入集合 \(Y\):
- 如果在 \(S\) 加入到 \(Y\) 之前,\(X\) 中已经存在一个字符串是它的前缀了,那么 \(S\) 的加入并不会导致答案发生改变。
- 而如果在 \(S\) 加入到 \(Y\) 之前,\(X\) 中不存在任何字符串是它的前缀,记这次操作是第 \(p\) 次操作,说明从第 \(p\) 个位置开始,答案会 \(+1\)。然后我们可以直接考虑在此之后,是否会有 \(S\) 的某个前缀被加入到 \(X\) 内:
- 如果没有,说明从 \(p\) 位置开始直到 \(Q\) 次询问结束,每个位置的答案均会 \(+1\)。
- 如果有,记最早出现的那个前缀出现在第 \(q\) 次操作内,那么在 \([p, q-1]\) 范围内的每个答案才会 \(+1\)。
首先考虑区间更新答案这一步,明显开一个差分数组即可维护。
然后我们将所有操作全部离线存储下来,然后对于每个加入到集合 \(X\) 的字符串,记录它加入的时间(记操作编号),用 map
或 unordered_map
来维护 <字符串,时间> 的关系。为优化查找过程的时间复杂度,需要用字符串哈希方法将字符串处理成一个整数,维护的则是 <字符串哈希值,时间> 的关系。由于可能会输入重复的字符串,根据题意,我们需要记录的是 \(X\) 中每种字符串出现的最早时间。
而对于每一次加入集合 \(Y\) 的字符串 \(S\),取出 \(S\) 的所有前缀,也用哈希值表示,然后在 map
容器内查找该前缀是否出现过。如果出现过,记录所有前缀当中出现的最早时间,也就是上面讨论过程中要求的 \(q\)。
当然这里需要注意,有可能 \(q\) 会早于当前字符串加入 \(Y\) 的时间 \(p\),特判一下。
代码
#include<bits/stdc++.h>
using namespace std;
typedef unsigned long long ull;
int q;
int op[200005];
string s[200005];
unordered_map<ull, int> mp; // 统计 X 中每个字符串哈希值分别在何时开始出现
int d[200005]; // 差分,统计每次询问后有多少个 Y 存在 Xi 作为前缀
int main()
{
int q;
cin >> q;
for(int i = 1; i <= q; i++)
cin >> op[i] >> s[i];
for(int i = 1; i <= q; i++)
{
if(op[i] == 1) // 只统计 X 集合内的字符串
{
ull hsh = 0;
for(char &c : s[i])
hsh = hsh * 131 + c;
if(mp.count(hsh) == 0)
mp[hsh] = i; // 记录每个字符串的哈希值出现的最早时间
}
}
for(int i = 1; i <= q; i++)
{
if(op[i] == 2) // 只处理 Y 集合内的字符串
{
ull hsh = 0;
int first = q + 1; // 记录所有前缀中出现的最早时间
for(char &c : s[i])
{
hsh = hsh * 131 + c;
if(mp.count(hsh) != 0)
first = min(first, mp[hsh]);
}
first = max(first, i);
d[i]++;
d[first]--;
}
}
for(int i = 1; i <= q; i++)
{
d[i] += d[i - 1];
cout << d[i] << "\n";
}
return 0;
}
F - Shortest One Formula
题意
给定一个正整数 \(N\),请求出一个最短的字符串,满足:
- 字符串仅由
1
,+
,*
,(
,)
组成 - 字符串看作一个数学表达式,表达式的结果恰好等于 \(N\)
- 表达式合法,具体可以参考题意中的描述,即表达式符合日常写法,且第一个数字前不能有符号
答案不唯一,输出任意一个即可。
思路
我们可以依次求出表达式的值为 \(i = 1\sim N\) 中的每个整数时的最佳答案。
特殊处理,当 \(i\) 仅由 \(1\) 组成时,表达式最优解即该数字本身。
此外,\(i\) 的答案一定可以从比 \(i\) 小的最优答案中,通过添加一些运算符来得到。
考虑动态规划,记 \(dp[i]\) 表示表达式值为 \(i\) 时的最优解。除上述特殊处理外,明显我们可以通过前面两个数字做加法或是做乘法来得到 \(i\)。
即 \(dp[i] = dp[j] + \text{“+”} + dp[i-j]\),或是 \(dp[i] = dp[j] + \text{“*”} + dp[\frac i j]\)。
但在乘法时有一个问题,如果现在要把两个表达式相乘,并且其中一边的表达式存在加号的话,那么这一边的表达式需要套一层乘号起来。
- 例如
dp[j] = "1 + 1"
,dp[i/j] = "11 * 11"
,此时如果中间以乘号连接,表达式应该写作(1 + 1) * 11 * 11
。
但我们又不能够通过“判断字符串内是否存在加号”来判断是否需要套一层括号,比如这个加号已经被某个括号括起来了。
- 例如
dp[j] = "(1 + 1) * 11"
,dp[i/j] = "11 * 11"
,此时如果中间以乘号连接,表达式应该写作(1 + 1) * 11 * 11 * 11
。
并且还可能出现一种情况,目前 dp[j]
在最外层表达式出现 +
时可能是局部最优解,但本次转移会因为加上一对括号而导致答案长度 \(+2\)。但如果 dp[j]
此前存在一种方案,最外层表达式没有出现 +
,但长度比当前的方案多 \(1\),明显此时我们是不需要加括号的,可能可以取到更优秀的解。
因此我们这样定义 dp 状态是不够的,需要再加一维,表示此时表达式最外层是否是使用 +
连接的。
定义 \(dp[i][0/1]\) 表示表达式值为 \(i\),且最外层 \(j=0\) 不是 +
连接,\(j=1\) 是 +
连接。
此时除了上面说的全 \(1\) 特殊处理以外,可以得出状态转移方程如下(注意其中的 \(\min\) 表示的是取最短长度的答案):
最后答案即 \(\min(dp[N][0], dp[N][1])\)。
代码
#include<bits/stdc++.h>
using namespace std;
string dp[2005][2];
// 如果 y 长度比 x 小,则将 x 改为 y,引用传递
void checkAns(string &x, string &y)
{
if(x.empty() || y.size() < x.size())
x = y;
}
// 取 x 和 y 较短的字符串
string checkMin(string &x, string &y)
{
if(x.size() < y.size())
return x;
return y;
}
int main()
{
int n;
cin >> n;
string blank(100, '.'); // 最开始将整个数组初始化为一个较长的无用字符串,方便状态转移
for(int i = 0; i <= n; i++)
for(int j = 0; j <= 1; j++)
dp[i][j] = blank;
dp[1][0] = "1";
for(int i = 2; i <= n; i++)
{
int k = 1;
while(k < i)
k = k * 10 + 1;
if(i == k)
dp[i][0] = to_string(i); // 如果 i 完全由 1 组成,直接取 i 作为表达式
string t;
for(int j = 1; j <= i / 2; j++)
{
// j + (i-j)
t = checkMin(dp[j][0], dp[j][1]) + "+" + checkMin(dp[i - j][0], dp[i - j][1]);
checkAns(dp[i][1], t);
}
for(int j = 2; j * j <= i; j++)
{
// j * (i / j)
if(i % j != 0)
continue;
t = dp[j][0] + "*" + dp[i / j][0];
checkAns(dp[i][0], t);
t = "(" + dp[j][1] + ")*" + dp[i / j][0];
checkAns(dp[i][0], t);
t = dp[j][0] + "*(" + dp[i / j][1] + ")";
checkAns(dp[i][0], t);
t = "(" + dp[j][1] + ")*(" + dp[i / j][1] + ")";
checkAns(dp[i][0], t);
}
}
cout << checkMin(dp[n][0], dp[n][1]) << "\n";
return 0;
}
G - Odd Position Sum Query
题意
有一个空数组。有 \(Q\) 次操作,每次操作给定一个整数 \(y\),一开始的整数 \(z = 0\)。
以下操作强制在线处理:
- 将 \(y\) 改为 \(((y + z) \bmod 10^9) + 1\),然后将 \(y\) 加入到数组中,对数组进行排序。
- 求出数组中所有奇数位置上的元素总和,输出,并将整数 \(z\) 的值改为本次得到的总和。
思路
考虑动态开点线段树维护值域 \([1, 10^9]\),将定义域问题转为值域问题。
每个结点维护三个重要值:
- 当前结点代表的区间范围内共有多少个数,记作 \(siz\)
- 当前结点内所有奇数位数值总和,记作 \(sum1\)
- 当前结点内所有偶数位数值总和,记作 \(sum2\)
每次加入一个新数字 \(val\),借助 update
函数向下更新直到区间仅包含 \([val,val]\) 数字本身(如果没有子结点则新建),此时该结点区间内数字个数 \(+1\)。由于过程中可能会出现多个相同的数字需要加入,因此该叶子结点在处理奇偶位数值总和时需要根据此时的数字个数来判断。
最后考虑叶子结点向上传递答案,数字个数直接加法合并即可,至于 \(sum1\) 和 \(sum2\) 两个成员变量,进行分类讨论(当然只和左儿子内的数字个数有关):
- 如果左儿子区间内数字个数为偶数个,那么将左右两端区间合并后,右儿子的奇数位还是奇数位,右儿子的偶数位还是偶数位,此时:
父结点.sum1 = 左儿子.sum1 + 右儿子.sum1
父结点.sum2 = 左儿子.sum2 + 右儿子.sum2
- 如果左儿子区间内数字个数为奇数个,那么将左右两端区间合并后,右儿子的奇数位将会变成偶数位,右儿子的偶数位也会变成奇数位,此时 :
父结点.sum1 = 左儿子.sum1 + 右儿子.sum2
父结点.sum2 = 左儿子.sum2 + 右儿子.sum1
最后每次更新完成后,取根节点的 \(sum1\) 即最终答案。
代码
#include<bits/stdc++.h>
using namespace std;
const int mod = 1e9;
struct node
{
int l, r; // 当前点维护的线段
int ls, rs; // 左右儿子的下标
int siz; // 当前区间内的数字个数
long long sum1, sum2; // 奇数位置与偶数位置上的数字总和
} tr[32 * 300005];
int tot = 0; // 记录目前线段树内的点数
// 创建新结点
int newNode(int l, int r)
{
tot++;
tr[tot].l = l;
tr[tot].r = r;
return tot;
}
// 上传两子结点值
void push_up(int p)
{
int &ls = tr[p].ls, &rs = tr[p].rs;
tr[p].siz = tr[ls].siz + tr[rs].siz;
if(tr[ls].siz % 2 == 1) // 根据左儿子线段内部数字个数来判断取右儿子的奇数位还是偶数位合并
{
tr[p].sum1 = tr[ls].sum1 + tr[rs].sum2;
tr[p].sum2 = tr[ls].sum2 + tr[rs].sum1;
}
else
{
tr[p].sum1 = tr[ls].sum1 + tr[rs].sum1;
tr[p].sum2 = tr[ls].sum2 + tr[rs].sum2;
}
}
// 将值 val 所在点 +1
void update(int val, int p = 1)
{
int &l = tr[p].l, &r = tr[p].r;
if(l == r)
{
tr[p].siz++;
// 同一个数字可能出现多次,根据出现次数判断加到奇数位还是偶数位
if(tr[p].siz % 2 == 1)
tr[p].sum1 += val;
else
tr[p].sum2 += val;
return;
}
int mid = (l + r) / 2;
if(val <= mid)
{
if(tr[p].ls == 0)
tr[p].ls = newNode(l, mid);
update(val, tr[p].ls);
}
else
{
if(tr[p].rs == 0)
tr[p].rs = newNode(mid + 1, r);
update(val, tr[p].rs);
}
push_up(p);
}
int main()
{
long long z = 0;
newNode(1, mod);
int q;
cin >> q;
while(q--)
{
long long y;
cin >> y;
y = (y + z) % mod + 1;
update(y);
z = tr[1].sum1;
cout << z << "\n";
}
return 0;
}