AGC007 DEF 题解

First Post:

Last Update:

AGC007D

简单题,虽然对数轴上游走的题还是不太会。

一眼看出了状态,转移懒得编了(以后还是要编转移,现在编转移的能力有些弱)。

容易发现,路径一定是先往前走一段,再折回一段,再往前走,走了一段再折,再走。

说白了,一个点要么被经过一次,要么被经过三次。

那么设 表示收到前 个糖果的最小时间,此时人一定在

那么枚举位置 中的点被一次折返收完,那么有转移

写出这个转移式子之后,优化非常简单,分类讨论那个 取到谁即可。因为 均不降,转移容易。

AGC007E

好题。

题意:给出一棵二叉树,每个节点只有 个儿子。

要求你从 走回 ,每天你可以从二叉树的一个叶子走到另一个叶子,或从 走到某个叶子,或从某个叶子走到 。要求你经过二叉树上的所有叶子,且每条边被经过恰好两次。

你的花费是最大的,从叶子到叶子的那一天经过的边的权值和,求最小花费。

首先二分答案,然后设 状态 表示从 开始游走,能否在第一天走过不超过 的距离,在第二天走过不超过 的距离。

那么有转移

交换 后再转移一遍即可。

当时只是觉得,这个转移形式有点双指针,然后想着能不能只记录一维,觉得挺假就没往下想了。

事实证明,确实需要双指针。

首先,对于同为 的状态 ,如果 ,那么后者完全无用。

所以你按 升序排序之后, 一定是降序的。

显然,对于每一个 都只需要考虑一个令 最小且满足转移式的 。所以,每一个 中的状态都只需要与一个对应的 转移到 中的唯一一个状态。所以,设左右子树的状态数分别为 ,那么一次转移只会增加最多 个状态(因为要把 交换后再转移一遍,所以要乘个 )。

根据启发式合并的复杂度分析,总状态数是 的。

找到每个状态对应的状态可以使用双指针,总时间复杂度是 的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
#include <bits/stdc++.h>
using namespace std;
const int N = (1 << 17) | 5;
typedef long long ll;
typedef pair<ll,ll> pll;
#define FI first
#define SE second
int n;
int son[N][2],w[N][2];

vector<pll> st[N]; // 状态集合
void DP(int x,ll mid)
{
st[x].clear();
if(!son[x][0]) {st[x].emplace_back(0,0);return;}
DP(son[x][0],mid);DP(son[x][1],mid);
ll tmp = mid - w[x][0] - w[x][1];
vector<pll> vec;
int num = 0;
for(int d = 0;d < 2;d++)
{
int lc = son[x][d],rc = son[x][d ^ 1];
for(int i = 0,j = 0;i < st[lc].size();i++)
{
while(j + 1 < st[rc].size() && st[rc][j + 1].FI + st[lc][i].SE <= tmp) ++j; //双指针
if(j >= st[rc].size() || st[lc][i].SE + st[rc][j].FI > tmp) continue;
vec.emplace_back(w[x][d] + st[lc][i].FI,w[x][d ^ 1] + st[rc][j].SE);
}
if(d == 0) num = vec.size();
}
inplace_merge(vec.begin(),vec.begin() + num,vec.end()); // 用归并来合并两个状态序列。用 sort 是 3 log 的。
for(int i = 0;i < vec.size();i++) // 删去无用状态
{
if(st[x].size() && st[x].back().SE <= vec[i].SE) continue;
st[x].push_back(vec[i]);
}
}
inline bool Check(ll mid)
{
DP(1,mid);
return st[1].size() != 0;
}
int main()
{
cin >> n;
ll lef = 0,righ = 0;
for(int i = 2;i <= n;i++)
{
int fa,v;
cin >> fa >> v;righ += v;
if(!son[fa][0]) son[fa][0] = i,w[fa][0] = v;
else son[fa][1] = i,w[fa][1] = v;
}
while(lef < righ)
{
ll mid = lef + righ >>1;
if(Check(mid)) righ = mid;
else lef = mid + 1;
}
cout << lef << endl;
return 0;
}

AGC007F

题意:

Shik 需要抄写一个字符串,一开始,纸面上有一个字符串 ,每次她写下字符串 的时候, 有可能与 相同,也可能与 相同。

Shik 希望抄写尽量少的字符串,就能使最后再纸上的字符串与 相同,具体地,她希望找到最小的整数 ,使得存在一种方案让

首先把图画出来, 中的一个字符肯定对应的是 中的一段区间。每个字符相当于在平面上向右或向下走。

当时的直觉是从后往前做,每个字符的匹配段都往右靠一定不劣,然后根据每个字符对应的区间设计 DP。

事实上,这样设计 DP 反倒会丢失路径信息。

考虑我们在不改变每个字符所对应区间的情况下,把中间走过的路径尽量往右靠,那么一定不劣。

而具体的策略就是:一直尽量右移,直到走到要覆盖的左边界,然后一直向下,最后横向覆盖。

有图如下(来自 @command_block):

考虑从右往左,从一条折线变到另一条折线时的变化。

假设当前 要到达 是区间左边界)

那么那些横坐标大于等于 的拐点要弹出。

然后,原来所有的拐点都会往左下方平移一格(因为新折线要尽量贴近原来的折线)。

最后,如果 ,那么在横坐标为 的地方会有一个拐点。

(上述所有拐点均不包括最后一行)。

我们可以用一个队列来维护拐点集合,队头是更加靠近目标串的,队尾是更加靠近 的。

上述操作都可以用队列维护。

核心代码如下:

1
2
3
4
5
6
7
8
9
10
11
while(down)
{
while(down > 1 && t[down - 1] == t[down]) --down;
while(up >= 1 && (up > down || s[up] != t[down])) --up;
if(!up) { puts("-1");return 0;}
while(!Q.empty() && Q.front() - (int)Q.size() >= down) // 这里减去 Q.size 是因为,每加入一个拐点,已有的拐点都会往左下方平移一格,这是本题所维护的折线的性质。
Q.pop();
if(up != down) Q.push(up);
ans = max(ans,(int)Q.size() + 1);
--down;
}

答案是队列大小 +1 的历史最大值。

因为可以归纳证明,每条折线的拐点的横坐标都是连续的(即都位于连续的前几行)。

大致是说,这对于最右的折线显然成立,而其它的折线都会尽量往右靠。

一道思路比较清奇的题。