2023 11月 AtCoder 做题记录
AGC037F
第一步,考虑判断序列是否合法。
通过对于属于等级 $k$ 的定义将定义反推:$s$ 中最小的元素 $x$,找到所有 $x$ 的连续段。设一个连续段的长度是 $len$,若 $len < l$ 则不合法,否则将这一段合并为 $\lfloor \frac {len}l \rfloor$ 个 $x + 1$,直到 $s$ 中只剩下一种元素,同时为了满足这个定义,这个元素一定是原 $s$ 最大值,且不再合并这个元素。
令 $s$ 最大值为 $m$,显然,合法情况有两种:
- 1. 若 $m$ 的数量为 $1$,这种情况当且仅当原序列的长度为 $1$,$s$ 属于等级 $m$,合法。
- 2. 若 $m$ 的数量不少于 $l$,$s$ 属于等级 $m + 1$,合法。
考虑将这个过程应用到个序列 $a$ 中。
从小到大考虑 $a$ 中出现过的值 $m$,当考虑到 $m$ 时,计算 $a$ 中所有最大值为 $w$ 的子段的中合法的数量,用栈维护。
现在,转化为问题:有一个序列 $s = s_0ms_1m \cdots $,其中 $s_i$ 为一个所有元素都 $<m$ 的序列,求这个序列中有多少个子段满足其属于等级 $m+1$。
设 $w$ 为 $s$ 中的最大值,对于一个序列 $s$ 维护信息:
- $f_{s,i}$ 表示 $s$ 中至多能合并出 $i$ 个 $w$ 的前缀个数。
- $g_{s,i}$ 表示 $s$ 中至多能合并出 $i$ 个 $w$ 的后缀个数。
- $h_{s}$ 表示 $s$ 至多能合并出的 $w$ 的个数(由 $f,g$ 求出)。
根据 $f_s,g_s,h_s$,可求出 $∀w^\prime > w$,$s$ 中至多能合并出 $i$ 个 $w^\prime$ 的前缀与后缀个数,$s$ 至多能合并出的 $w^\prime$ 的个数。
假设我们现在对于 $s = s_0ws_1w \cdots ws_m$ 中的所有 $s_i$ 都求出了 $f_{s_i},g_{s_i},h_{s_i}$,则我们首先可以求出 $s_i$ 中至多能合并出 $i$ 个 $w$ 的前/后缀个数,以及 $s$ 至多能合并出的 $w$ 的个数。之后就可以求出答案了。
时空复杂度 \(O(n\) \(logn)\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 2e5 + 5;
int n, l;
int s[N], f[N], g[N];
int sf[N], sg[N];
int ans;
int top;
void pop()
{
int k = 1;
int x = s[top];
while (s[top - 1] == x) top--, k++;
top--;
if (k < l)
{
while (top != 0) pop();
return;
}
for (rint i = 1; i <= k; i++)
{
sf[i] = sf[i - 1] + f[i + top];
sg[i] = sg[i - 1] + g[i + top];
}
for (rint i = l; i <= k; i++)
{
ans += sf[i - l + 1] * g[i + top];
}
int m = k / l;
for (rint i = 1; i <= m; i++)
{
f[top + i] = sf[k - l * (m - i + 1) + 1] - sf[max(k - l * (m - i + 2) + 1, 0ll)];
g[top + i] = sg[min(l * (i + 1) - 1, k)] - sg[l * i - 1];
}
int p = 0;
for (rint i = l; i <= m; i++)
{
p += f[top + i - l + 1];
ans -= p * g[top + i];
}
for (rint i = 1; i <= m; i++)
{
s[++top] = x + 1;
}
}
signed main()
{
cin >> n >> l;
ans = n;
for (rint i = 1; i <= n; i++)
{
int a;
cin >> a;
while (top != 0 && s[top] < a) pop();
s[++top] = a;
f[top] = g[top] = 1;
}
while (top != 0) pop();
cout << ans << endl;
return 0;
}
ARC102F
题目传送门
题目大意为,给定长度为 \(n\) 的排列 \(p\), 可以进行无限次操作, 问最终能否将其排成升序. 其中, 一次操作定义为选择 \(i\) 使得 $ 2 \leq i \leq n-1$ 且 \(p_{i-1}>p_i>p_{i+1}\). 交换 \(p_{i-1},p_{i+1}\)
在上一道题的求解中,用到了逆向思维,这道题同样如此。
对于此题,假设 择 \(i\) 使得 $ 2 \leq i \leq n-1$ 且 \(p_{i-1}>p_i>p_{i+1}\). 交换 \(p_{i-1},p_{i+1}\) 中,\(i\) 为合法点。如果 \(p_i \ne i\) 且 \(i\) 为合法点,显然,无论怎样操作都会自相矛盾对吧,发现, 若 \(i\) 为合法点, 则在所有操作之前, \(p_i=i\)
通过这个性质我们可以先筛掉一些一定不合法的序列。
但是只通过这个性质并不全。
定义数组 bool a[i]=[i==p[i]]
, 如果 \(a\) 中有连续的 \(3\) 个 \(0\) 出现, 无解. 考虑将 \(a\) 分成若干个区间, 使得同一个区间内没有两个连续的数相等. 易证区间与区间之间不存在数的交换。个区间的值域的范围和下标的范围应该要一样. 在一个区间内, 考虑把 \(a_i=0\) 的数单独拿出来, 如果其最长下降子序列的长度大于等于三则无解. 考虑有 \(3\) 个数 \(a>b>c\), 显然它们是无法交换过来的.
此做法时空复杂度 \(O(n)\)
#include <bits/stdc++.h>
#define rint register int
#define int long long
#define endl '\n'
using namespace std;
const int N = 5e5 + 5;
int n, p[N];
bool a[N];
int minn[N];
int s[N], top;
signed main()
{
cin >> n;
p[n + 1] = n + 1;
for (rint i = 1; i <= n; i++)
{
cin >> p[i];
}
for (rint i = 1; i <= n; i++)
{
if (p[i - 1] != i - 1 && p[i] != i && p[i + 1] != i + 1)
{
puts("No");
return 0;
}
else if (i == p[i])
{
a[i] = 1;
}
}
rint j = 0;
for (rint i = 1; i <= n; i = j + 1)
{
j = i;
while (j < n && a[j] != a[j + 1]) j++;
top = 0;
for (rint k = i; k <= j; k++)
{
if (p[k] < i || j < p[k])
{
puts("No");
return 0;
}
if (!a[k])
{
s[++top] = p[k];
}
}
minn[top] = s[top];
for (rint k = top - 1; k >= 1; k--)
{
minn[k] = min(minn[k + 1], s[k]);
}
int maxx = s[1];
for (rint k = 2; k < top; k++)
{
maxx = max(maxx, s[k]);
if (s[k] > minn[k] && s[k] < maxx)
{
puts("No");
return 0;
}
}
}
puts("Yes");
return 0;
}
AGC039D
傻逼数学题平面几何
结论:对于任意 \(\triangle ABC\),三段弧中点的坐标之和,就是 \(\triangle ABC\) 内心的坐标。
由垂心角平分线定理和欧拉线很容易就能整出来。代码不粘了。
AGC021E
很好的一道组合数学题:
我们可以先手搓几个样例玩玩,通过总结,发现最多能喂 \(k\) 只变色龙的都是这样的序列:先是一段 \(A\),然后一段一段 \(AB\) 相连的东西(严格来说是 \(BA\) 相连......)
然后最后一段先是 \(A\) 后是 \(B\)
如果最后喂给那只被钦定的变色龙的 \(A<B\) 时,最多只能弄出 \(x\) 只红变色龙,显然,手上的那个球只能是 \(A\)。否则要拯救那只变色龙,手上的球只能是 \(B\)
通过 $x-1 $个 \(B\) 的位置确定整个序列
方案数是 \(C^{k-1}_{x-1}\)
ABC197F
回文字符串的特点是对称性,不难想到这个题是双向搜索
先考虑长度为偶数的情况,初始两个位置分别是 \(1,n\),每次两个位置各选择一条连出的边,满足边上的字母相同,并向另一个端点移动,最终两个位置重合。
设这个相同的位置是 $k$,此时 $1\rightarrow k$ 的路径和 $n\rightarrow k$ 的路径所形成的字符串相同。将 $n\rightarrow k$ 反过来,再在前面接上 $1\rightarrow k$,最终会得到一条满足条件的路径。把两个位置放进状态中,再 bfs。
奇数的情况,找到两个位置之间恰好隔了一条边的情况即可
AGC022F
题目传送门
一道不错的 \(dp\),但是实力不够,学了一下 APijifengC 的题解。
得到的数肯定都是 \(\sum 2^a(-1)^bx^i\) 的形式,只关心最后得到的系数的集合即可。
考虑这个集合的性质:
- \(\forall i\in A,\, \exists j,\, \rvert i\lvert=2^j\) ,即集合中每个元素的绝对值都是 \(2\) 的次幂。
- \(∑ _{i∈A}i=1\),即集合中元素的和为 \(1\)
- 若 \(2^i\) 或 \(-2^{i}\) \(∈A\),那么\(2^{i-1}\) 或 \(-2^{i-1}\) \(∈A\)
- \(1\) 和 \(-1\) 中有一个且仅有一个属于 \(A\)
- 所有指数小于等于 \(i\) 的数的和为 \(1\)
设 \(f_{i,j}\) 表示已经放了 \(i\) 个元素,并且这些元素的和为 \(1+jV\),其中 \(V\) 为现在要放的 \(2\) 的次幂是多少。
枚举选了 \(a\) 个 \(V\),选了 \(b\) 个 \(−V\),元素的和变为 \(1+(j+a-b)V\)。因为下一个填的数就要比现在的数乘 \(2\),所以 \(f_{i,j}\) 转移到 \(f_{i+a+b,\frac{j+a-b}{2}}\)。
需要满足将这些数任意标正负号之后,和为 \(1\),那只需要满足 \(j \equiv a+b \pmod 2\) 且 \(a+b\ge \rvert j\lvert\)
然后考虑对这个系数集合分配原来的 \(x^i\),这个就是多重组合数的形式 ($ \frac{n!}{\prod a!}$),把 \(\frac{1}{\prod a!}\) 的部分拆到转移上,即 \(f_{i,j} \times \frac{1}{a!b!}\rightarrow f_{i+a+b,\frac{j+a-b}{2}}\)
初始状态为 \(f_{0,-1}=1\),答案 \(n!f_{n,0}\)。
signed main()
{
cin >> n;
f[0][N - 1] = 1;
init();
for (rint i = 0; i < n; i++)
{
for (rint j = -n; j <= n; j++)
{
if (f[i][j + N])
{
for (rint a = 0; a <= n; a++)
{
for (rint b = 0; i + a + b <= n; b++)
{
int s = a + b;
int x = i + s;
int y = (j + a - b) >> 1;
if ((a || b) && !((j + a + b) % 2) && abs(j) <= a + b)
{
if (i == 0 && a + b != 1)
{
continue;
}
f[x][y + N] = (f[x][y + N] + f[i][j + N] * inv[a] % P * inv[b]) % P;
}
}
}
}
}
}
cout << f[n][N] * fac[n] % P << endl;
return 0;
}
AGC056D
\(n\) 是奇数时,枚举 Alice 第一次选的数,剩下的数两两匹配,若 Bob 选了其中一个,Alice 就选另一个。
显然,奇数位和它右边的偶数位匹配最优。
设奇数位的和为 \(q\),偶数位的和为 \(p\),此时 Alice 获胜当且仅当 \(l \le q + a_i \land p + a_i \le r\)。
\(n\) 是偶数时,仍匹配。枚举 Alice 第一次选的数和这个数匹配的数,剩下的数仍是奇数位匹配右边偶数位最优。
若 Bob 选了这个数匹配的数,Alice 可以新开一个匹配,否则 Alice 选 Bob 选的数匹配的数即可。
想法很好,但代码我写出来的很冗长,就不放了。
AGC027E
考虑对字母进行赋值计算即可。
不妨设,\(a = 1, b = 2\) ,显然,操作之后,字符的值之和模 \(3\) 都是固定的。因此,可以便捷的求出之后会变成哪个字符。
考虑 dp 状态, 加上除了自己外的其它 \(f\) 值之和,\(f_{p_0} + f_{p_1} + f_{p_2} - f_{p_{s_i}}\),其中 \(p_i\) 表示上一个为 \(i\) 的字符的位置。最后答案为 f[n]
const int N = 1e5 + 5;
const int mod = 1000000007;
int n, ans;
int f[N], p[N], s[N];
char c[N];
bool check(char c[], int n)
{
bool flag = 0;
for (rint i = 1; i < n; i++)
{
if (c[i] == c[i + 1])
{
flag = 1;
}
}
return flag;
}
signed main()
{
cin >> c + 1;
n = strlen(c + 1);
if (!check(c, n))
{
cout << 1 << endl;
return 0;
}
for (rint i = 1; i <= n; i++)
{
s[i] = (s[i - 1] + c[i] - 'a' + 1) % 3;
}
for (rint i = 1; i <= n; i++)
{
if (s[i])
{
f[i]++;
}
f[i] = (f[i] + f[p[0]] + f[p[1]] + f[p[2]] - f[p[s[i]]]) % mod;
p[s[i]] = i;
}
cout << f[n] << endl;
return 0;
}
ARC110E
和 AGC027E 整体做法和思路是一样的
现将字母转换成数字,\(A=1,B=2,C=3\)
给定的串分成好几段,每段异或和不为 \(0\),且不全是同一种字符(\(len≠1\))
每个段分别合并成一个字符,这样一个合法的划分方案就对应一个答案,注意,一个答案可以由若干个合法方案得到。
记 \(f_i\) 表示 \(S\) 的后缀 \(i\) 的答案。
转移方程为: \(f_i =[xor(i,n)=0] + f_{g_{i,1}+1} + f_{g_{i,2}+1} + f_{g_{i,3}+1}\) ,\(xor(a,b)\) 表示 \(a\) 到 \(b\) 的异或和。\(g_{i,j}=\min_{i \le r \le n} \{r|xor(i,r)=j\}\),即从 \(i\) 开始最短的一段拼出 \(j\) 的前缀。最后答案为 \(f[1]\)
const int N = 1e6 + 5;
const int mod = 1000000007;
int n;
int c[N], f[N];
int g[N][4];
int tag;
bool check(int c[], int n)
{
bool flag = 0;
for (rint i = 1; i < n; i++)
{
if (c[i] != c[i + 1])
{
flag = 1;
}
}
return flag;
}
signed main()
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
char a;
cin >> a;
c[i] = a - 'A' + 1;
//cout << c[i] << endl;
}
if (!check(c, n))
{
cout << 1 << endl;
return 0;
}
for (rint i = n + 1; i != 0; i--)
{
g[i][c[i]] = i;
g[i][c[i] ^ 1] = g[i + 1][1];
g[i][c[i] ^ 2] = g[i + 1][2];
g[i][c[i] ^ 3] = g[i + 1][3];
tag ^= c[i];
if (tag == 0) f[i] = 1;
if (tag != 0) f[i] = 0;
if (g[i][1])
{
f[i] += f[g[i][1] + 1];
f[i] %= mod;
}
if (g[i][2])
{
f[i] += f[g[i][2] + 1];
f[i] %= mod;
}
if (g[i][3])
{
f[i] += f[g[i][3] + 1];
f[i] %= mod;
}
}
if (tag == 0)
{
cout << f[1] - 1 << endl;
}
else
{
cout << f[1] << endl;
}
return 0;
}
AGC001F
刚开始想的是反向拓扑排序,后来发现时空复杂度是 \(O(NK)\),铁定超时。之后用线段树维护,预估复杂度能到 \(O(n\) \(logn)\),然后我写了两个小时没过样例。想看看题解有没有也用线段树+反向拓扑的。然后第一篇 linghuchong_巨佬 的题解用归并排序秒掉了,\(ORZ\)
构造序列 \(S\) 使得 \(S_{p_i}=i\),变成交换相邻的差值大于等于 \(k\) 的数。
想实现这个问题可以直接冒泡排序,把大的尽可能往右挪,时空复杂度 \(O(n^2)\)。
考虑优化,归并排序。一个右边的数能比左边的数先进行归并就要保证它加 \(k\) 小于等于左边的数的后缀最小值即可。每次归并时记录左边的后缀最小值。对于随机的数据,复杂度甚至接近 \(O(n)\)。
const int N = 5e5 + 5;
const int inf = 1e9;
int n, k;
int minn[N], pos[N];
int a[N], b[N];
void merge(int l, int r)
{
if (l == r)
{
return ;
}
int mid = (l + r) >> 1;
merge(l, mid);
merge(mid + 1, r);
int _min = inf;
int idx = l - 1;
int x = l, y = mid + 1;
for (rint i = mid; i >= l; i--)
{
_min = min(_min, a[i]),
minn[i] = _min;
}
while (x <= mid && y <= r)
{
if (minn[x] >= a[y] + k)
{
b[++idx] = a[y];
y++;
}
else
{
b[++idx] = a[x];
x++;
}
}
while (x <= mid)
{
b[++idx] = a[x];
x++;
}
while (y <= r)
{
b[++idx] = a[y];
y++;
}
for (rint i = l; i <= r; i++)
{
a[i] = b[i];
}
}
signed main()
{
cin >> n >> k;
for (rint i = 1; i <= n; i++)
{
int x;
cin >> x;
a[x] = i;
}
merge(1, n);
for (rint i = 1; i <= n; i++)
{
pos[a[i]] = i;
}
for (rint i = 1; i <= n; i++)
{
cout << pos[i] << endl;
}
return 0;
}
AGC052D
挺不错的一道题,顺手写个题解交了。
对于一个序列是否能分成两个 LIS 长度相等子序列。我们要注意审题,他说的是两个长度相等的对吧。
也就是说,当对于是整体最长上升子序列长度 \(len\),\(len\) 为偶数,即 !(len % 2)
的时候,这个显然是有解的。
然后呢?先不要着急考虑奇数的情况,对于刚才偶数的情况还没有处理完,这个 \(len\) 怎么计算呢。设 \(f_i\) 是以 \(i\) 为结尾的最长上升子序列长度,那么不难得出:
计算出 \(len\),就解决了偶数的情况。
接下来考虑奇数情况。\(g_i\) 表示以 \(i\) 为开头的最长上升子序列长度,我们不妨设 \(len = 2 × x + 1\),\(x∈N_+\)
两个子序列的最长上升子序列长度至少是 \(x + 1\),那么,是不是个 最长上升子序列之外的元素,存在某个长为 \(x + 1\) 的上升子序列覆盖着它呢?
设这个上升子序列 \(P\):\(P_1,P_2......P_{k+1}\)
对于一个序列,满足 \(f_i≠f_{p_k} or f_i=f_{j≠i}\) 且 \(k≤x+1\) 的是选取序列,剩下的为剩余序列。
对于无论是选取序列还是剩余序列,我们发现,有不超过 \(x + 1\) 个不同的以 \(i\) 为结尾的最长上升子序列长度对吧。所以,对于选取序列,最长上升子序列长度是要不小于 \(x + 1\) 的,并且剩余序列的最长上升子序列长度同样要不小于 $ x + 1$。
所以,求出 \(f_i,g_i\),如果 \(f_i + g_i ≥ len / 2 + 2\),累计一次答案,如果最后累计结果 \(cnt > len\),那么就有解,否则无解。
时空复杂度 \(O(n\) \(log\) \(n)\)
int T, n;
int p[N], id[N];
int f[N], g[N];
signed main()
{
cin >> T;
while (T--)
{
cin >> n;
for (rint i = 1; i <= n; i++)
{
cin >> p[i];
}
for (rint i = 1; i <= n; i++) id[i] = n + 1;
int len = 0;
for (rint i = 1; i <= n; i++)
{
f[i] = lower_bound(id + 1, id + n + 1, p[i]) - id;
id[f[i]] = p[i];
len = max(len, f[i]);
}
if (!(len % 2))
{
puts("YES");
continue;
}
for (rint i = 1; i <= n; i++) id[i] = n + 1;
for (rint i = n; i != 0; i--)
{
g[i] = lower_bound(id + 1, id + n + 1, n - p[i]) - id;
id[g[i]] = n - p[i];
}
int cnt = 0;
for (rint i = 1; i <= n; i++)
{
if ((f[i] + g[i]) >= (len / 2 + 2))
{
cnt++;
}
}
if (cnt > len)
{
puts("YES");
}
else
{
puts("NO");
}
}
return 0;
}
ARC107D
定义 \(f_{i,j}\) 为选了 \(i\) 个数,和为 \(j\)。这里不需要考虑分数造成的影响,这种影响在转移方程时会被弥补掉。
考虑如何转移:
- \(f_{i-1,j-1}\) \(→\) \(f_{i,j}\)
- \(\frac{1}{2^i}\) 可以被选,所以所有的数可都翻倍
综上,转移方程为 \(f_{i,j} = f_{i - 1,j - 1} + f_{i,j} * 2\)
signed main()
{
cin >> n >> k;
f[0][0] = 1;
for (rint i = 1; i <= n; i++)
{
for (rint j = i; j >= 1; j--)
{
f[i][j] = f[i - 1][j - 1];
int k = j << 1;
if (i >= k)
{
f[i][j] += f[i][k];
}
f[i][j] %= mod;
}
}
cout << f[n][k] << endl;
return 0;
}
ARC159D
$f_i$ 表示以 $r_i$ 结尾的 LIS 长度。转移方程为 $f_i=\max(\max\{r_i+f_j-r_j|r_j\ge l_i\},\max\{r_i-l_i+1+f_j|r_j<l_i\})$。
然后离散化 $l_i$ 和 $r_i$ 后用两棵线段树维护 $f_i$ 和 $f_i-r_i$ 的最大值。
AGC007D
\(f[i]\) 表示收到前 \(i\) 个糖果的最小时间,其必然在第 \(x[i]\) 这个位置。
\(f[i]=\min(f[j]+x[i]-x[j]+\max(2\times(x[i]-x[j+1]),T))\)。
对于 \(x[i]-x[j]\) ,所有 \(f[n]\) 必然是从头走到尾,所以要加上 \(x[n]\)
时空复杂度 \(O(n)\)
signed main()
{
cin >> n >> m >> t;
for (rint i = 1; i <= n; i++)
{
cin >> a[i];
}
int j = 0;
for (rint i = 1; i <= n; i++)
{
while ((a[i] - a[j + 1]) * 2 >= t && j < n)
{
minn = min(minn, f[j] - 2 * a[j + 1]);
j++;
}
f[i] = min(2 * a[i] + minn, f[j] + t);
}
cout << f[n] + m << endl;
return 0;
}
ARC064E
圆外点到圆内的距离最短:和圆心连线。
将所有圆都看作圆心一个点,求出的距离必定是最短距离。
给任意两点间建边,边权就是圆心距离减去两个圆的半径,之后最短路即可。
邻接矩阵朴素 djkstra 就能过,没必要堆优化,时空复杂度 \(O(n^2)\)
signed main()
{
cin >> x[0] >> y[0] >> x[1] >> y[1] >> n;
for (rint i = 0; i < n; i++)
{
cin >> x[i + 2] >> y[i + 2] >> r[i + 2];
}
n += 2;
for (rint i = 0; i < n; i++)
{
for (rint j = i + 1; j < n; j++)
{
double d = sqrt((x[j] - x[i]) * (x[j] - x[i]) + (y[j] - y[i]) * (y[j] - y[i]));
a[i][j] = a[j][i] = max(0.0, d - r[i] - r[j]);
}
}
for (rint i = 1; i < n; i++)
{
dist[i] = inf;
}
for (rint j = 0; j < n; j++)
{
int x = -1;
for (rint i = 0; i < n; i++)
{
if (!v[i] && (x == -1 || dist[i] < dist[x]))
{
x = i;
}
}
v[x] = 1;
for (rint i = 0; i < n; i++)
{
dist[i] = min(dist[i], dist[x] + a[x][i]);
}
}
cout << fixed << setprecision(15) << dist[1] << endl;
return 0;
}