2021牛客寒假算法基础集训营4 解题补题报告
A题 九峰与签到题 (签到)
这签到题难度就比别的签到题高一大截:按顺序记录每题的通过率,只有全部时刻正答率在 \(50\%\) 上才算签到(我觉得按照这个标准,这题显然不是)。
#include<bits/stdc++.h>
using namespace std;
int n, m;
int cnt1[30], cnt2[30];
bool vis[30], ans[30];
int main()
{
cin>>m>>n;
for (int i = 1; i <= n; ++i)
ans[i] = true;
for (int i = 1; i <= m; ++i) {
int x;
string s;
cin>>x>>s;
vis[x] = 1;
cnt1[x]++;
if (s == "AC") cnt2[x]++;
if (2 * cnt2[x] < cnt1[x]) ans[x] = false;
}
bool flag = 0;
for (int x = 1; x <= n; ++x)
if (vis[x] && ans[x]) {
printf("%d ", x);
flag = 1;
}
if (!flag) printf("-1");
return 0;
}
B题 武辰延的字符串 (二分,字符串哈希)
我一开始直接想出来 \(O(n^3)\) 的算法,直接扔了(逃
-
字符串哈希
对于字符串的比较(尤其是不断对一个字符串的子串的比较),我们不妨使用字符串哈希来进行优化,将复杂度从 \(O(n)\) 降到 \(O(1)\)。
-
二分
这个 \(O(n^2)\) 的枚举似乎是无法优化掉的,但是嘛......
我们假设 \(s_i+s_j=t_{i+j}\),且 \(s_i+s_{j+1}\not= t_{i+j+1}\)(不)难发现:
- 对于 \(1\leq k \leq j\),有 \(s_i+s_k=t_{i+k}\)
- 对于 \(k > j\),有 \(s_i+s_k\not=t_{i+k}\)
这个是一个显然的二分性质,所以我们可以通过二分,将第二维的枚举从 \(O(n)\) 降到 \(O(\log n)\) 。
综上,总复杂度 \(O(n\log n)\),可以通过本题。
#include <bits/stdc++.h>
using namespace std;
const int N = 100010;
#define LL long long
int n, m;
char s[N], t[N];
//HASH
#define ULL unsigned long long
const ULL BASE = 131;
ULL hash_s[N], hash_t[N], p[N];
//
int calc(int i) {
int l = 0, r = m - i;
while (l < r) {
int mid = (l + r + 1) >> 1;
if (hash_s[mid] + hash_s[i] * p[mid] == hash_t[i + mid]) l = mid;
else r = mid - 1;
}
return l;
}
int main()
{
//INIT
p[0] = 1;
for (int i = 1; i < N; i++)
p[i] = p[i - 1] * BASE;
//INPUT
scanf("%s%s", s + 1, t + 1);
//HASH
n = strlen(s + 1), m = strlen(t + 1);
for (int i = 1; i <= n; i++)
hash_s[i] = hash_s[i - 1] * BASE + (s[i] - 'a' + 1);
for (int i = 1; i <= m; i++)
hash_t[i] = hash_t[i - 1] * BASE + (t[i] - 'a' + 1);
LL ans = 0;
//就离谱
//就是这个 i<=m 这个边界条件,让我调了两个小时就离谱
//严格来讲,是 i<m && i<=n
for (int i = 1; i<m && i<=n; i++)
{
if (s[i] != t[i]) break;
ans += calc(i);
}
printf("%lld", ans);
return 0;
}
C题 九峰与CFOP
大模拟,写的并不是很来(逃
D题 温澈滢的狗狗
E题 九峰与子序列
F题 魏迟燕的自走棋
如果数据范围小点的话,我想费用流水过去的,可惜小不得(逃
G题 九峰与蛇形填数 (数据结构)
暴力的复杂度是 \(O(mn^2)\),直接 \(T\) 飞(逃。
其实,对于 \(O(n^2)\) 的操作,我们可以将其视为 \(n\) 次复杂度为 \(O(n)\) 的区间修改,将某一段修改为一个等差数列。那么我们就可以尝试将这个修改的复杂度降到 \(O(\log n)\) 甚至 \(O(1)\) 。
可惜了,这题是修改,如果是加的话,这题就可以通过差分来做了,直接降到 \(O(1)\)(一道多次区间加等差数列的题目,这是题解)。
今天又看了下题解,发现一个恐怖的现实:似乎修改更简单(逃
对于一个矩阵,我们只需要给每个元素打上同样的标记,然后每个点都可以根据标记,从而 \(O(1)\) 的计算出这个点的值。(打个比方,我给一块区间打上 \(opt:(x,y,k)\) 的标记,那么对于每个点,我们就可以 \(O(1)\) 的推断出来,这个点的值应该是多少)。换言之,我们并不需要在每次修改的时候真的修改到每一个点的值,只需要在最后遍历的时候算出来即可(有点像铺地毯那题了?)
用线段树优化打标记的复杂度,可以将总复杂度压到 \(O(mn\log n)\),并不是很优秀,得花好一会功夫才能卡过去(平均提交 3 次过 1 次)(逃
#include<bits/stdc++.h>
using namespace std;
const int N = 2010, M = 3010;
//
int m;
struct opt {
int x, y, k;
}Opt[M];
int calc(int x, int y, int id) {
//这边做了好几个卡常数处理
if (id == 0) return 0;//注意没打标记的情况
x = x - Opt[id].x + 1, y = y - Opt[id].y + 1;
int &k = Opt[id].k;//引用,不建新变量
if (x & 1) return (x - 1) * k + y;//二进制而不是取模
else return (x - 1) * k + k - y + 1;
}
//
int n;
struct SegmentTree
{
struct node {
int l, r, val, tag;
} a[N << 2];
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
void build(int x = 1, int l = 1, int r = n) {
a[x].l = l, a[x].r = r;
if (l == r) return;
int mid = (l + r) >> 1;
build(ls(x), l, mid);
build(rs(x), mid + 1, r);
}
inline void f(int x, int k) {
a[x].val = a[x].tag = k;
}
void pushdown(int x) {
int &k = a[x].tag;
if (k) {
a[ls(x)].val = a[ls(x)].tag = k;
a[rs(x)].val = a[rs(x)].tag = k;
k = 0;
}
}
int query(int p, int x = 1) {
if (a[x].l == a[x].r) return a[x].val;
pushdown(x);
int mid = (a[x].l + a[x].r) >> 1;
if (p <= mid) return query(p, ls(x));
else return query(p, rs(x));
}
void update(int l, int r, int val, int x = 1) {
if (l <= a[x].l && a[x].r <= r) {
a[x].val = a[x].tag = val;
return;
}
pushdown(x);
int mid = (a[x].l + a[x].r) >> 1;
if (l <= mid) update(l, r, val, ls(x));
if (r > mid) update(l, r, val, rs(x));
return;
}
}arr[N];
int main()
{
//input
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; ++i)
scanf("%d%d%d", &Opt[i].x, &Opt[i].y, &Opt[i].k);
//solve
for (int i = 1; i <= n; ++i)
arr[i].build();
for (int i = 1; i <= m; ++i) {
int x = Opt[i].x, y = Opt[i].y, k = Opt[i].k;
for (int d = 0; d < k; ++d)
arr[x + d].update(y, y + k - 1, i);
}
//output
for (int i = 1; i <= n; ++i) {
for (int j = 1; j <= n; ++j)
printf("%d ", calc(i, j, arr[i].query(j)));
puts("");
}
return 0;
}
这题还有不少奇奇怪怪的打标记的方式,但是可能有点超出我的能力范围了,溜了溜了
H题 吴楚月的表达式 (树的遍历,表达式求值)
一道树的遍历题,需要在遍历时不断维护每个点的状态。
一个非空表达式前缀可以表示成 \(a+b\) 的形式。
如果后面接了一个 \(+x\),则变成 \((a+b)+x\) ;
如果后面接了一个 \(−x\),则变成 \((a+b)+(−x)\);
如果后面接了一个 \(∗x\),则变成 \(a+b∗x\) ;
如果后面接了一个 \(/x\),则变成 \(a+b/x\) 。
最后还是可以表示成 \(a+b\) 的形式。
因此只需要遍历整棵树维护每个节点对应的 \(a+b\) 即可。
(别问我咋知道这么想的,问就是看的答案)
#include<bits/stdc++.h>
using namespace std;
#define LL long long
const int N = 100010;
int n;
LL v[N];
const LL mod = 1e9 + 7;
//
LL quickpow(LL a, int x) {
if (x == 0) return 1;
if (x == 1) return a;
LL half = quickpow(a, x / 2);
if (x % 2 == 0) return half * half % mod;
return half * half % mod * a % mod;
}
LL Div(LL b, LL a) {
return b * quickpow(a, mod - 2) % mod;
}
//
int fa[N];
char opt[N];
vector<int> tree[N];
//
LL a[N], b[N];
void dfs(int x) {
for (int i = 0; i < tree[x].size(); ++i) {
int to = tree[x][i];
switch (opt[to]) {
case '+':
a[to] = (a[x] + b[x]) % mod;
b[to] = v[to] % mod;
break;
case '-':
a[to] = (a[x] + b[x]) % mod;
b[to] = (-v[to] + mod) % mod;
break;
case '*':
a[to] = a[x];
b[to] = b[x] * v[to] % mod;
break;
case '/':
a[to] = a[x];
b[to] = Div(b[x], v[to]);
break;
}
dfs(to);
}
}
int main()
{
//input
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%lld", &v[i]);
for (int i = 2; i <= n; ++i) {
scanf("%d", &fa[i]);
tree[fa[i]].push_back(i);
}
scanf("%s", opt + 2);
//solve
a[1] = 0, b[1] = v[1];
dfs(1);
//output
for (int i = 1; i <= n; ++i)
printf("%lld ", (a[i] + b[i]) % mod);
return 0;
}
I题 九峰与分割序列
J题 邬澄瑶的公约数 (数论)
我一开始还想着先把他们的 \(\gcd\) 求出来,然后操作操作,后来发现想太多了(逃
求 \(\gcd\),辗转相除是一个办法,但是我们不妨从另外一个角度(也就是定义)来求:对于这些数,分别进行质因数分解,然后对于每一个质因数,判断其出现次数的最小值,然后累乘即可。
例如 \(12=2^2*3,18=2*3^2,15=3*5\)
那么 \(\gcd(12,18,15)=2^0*3^1*5^0=3\)
那么我们只要依次实现三个部分:打出素数表(素数筛),质因数分解,累乘(快速幂)。
三个步骤的复杂度各不相同(素数筛的复杂度是 \(O(n\log\log n)\),质因数分解的复杂度是 \(O(n\sqrt{n})\),累乘的复杂度是 \(O(nm)\),\(m\) 是 \(10^4\) 以内的质数的数量),但是根据具体跑出来的数据,反正可以 \(A\) 掉这题。
#include<bits/stdc++.h>
using namespace std;
#define LL long long
//
const int N = 10010, C =1510;
const LL mod = 1e9 + 7;
LL quickpow(LL a, LL b) {
if (b == 0) return 1;
if (b == 1) return a;
LL half = quickpow(a, b / 2);
if (b % 2 == 0) return half * half % mod;
return half * half % mod * a % mod;
}
//
int cnt = 0;
int id[N], prime[N];
void GeneratePrime(int n) {
int vis[N];
memset(vis, 0, sizeof(vis));
for (int i = 2; i <= n; ++i) {
if (vis[i]) continue;
id[i] = ++cnt, prime[cnt] = i;
for (int j = 2; i * j <= n; ++j) vis[i * j] = 1;
}
}
void divide(int x, int *arr, int p) {
for (int i = 1; prime[i] <= x && i <= cnt; ++i)
if (x % prime[i] == 0) {
while (x % prime[i] == 0)
x /= prime[i], arr[i]++;
arr[i] *= p;
}
}
//
int n, a[N], d[N][C];
int main()
{
GeneratePrime(10000);
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
scanf("%d", &a[i]);
for (int i = 1; i <= n; ++i) {
int p;
scanf("%d", &p);
divide(a[i], d[i], p);
}
LL ans = 1;
for (int i = 1; i <= cnt; ++i) {
int Min = 100010;
for (int k = 1; k <= n; ++k)
Min = min(Min, d[k][i]);
ans = (ans * quickpow(prime[i], Min)) % mod;
}
printf("%lld", ans);
return 0;
}