Solution Set - 咋提玄坛 | 说无可说

liuzimingc / 2024-02-16 / 原文

说无可说。

I have nothing to say.

Mi havas nenion por diri.

J'ai rien à dire.

说无可说

Link。

我说,我去,暴力 dp 一次就是 \(O(|S| ^ 2)\) 的了,直接起飞!

题目说,我只要求相似度为 \(1 \sim 8\) 的字符串对数,there must be a reason。

我说,来可以 dfs,太奇啦!但是这要怎么搜啊?那么其实类似 dp 那么记录端点,,每次 dfs 转移下一个状态就是先暴力匹配,找到第一个依次推下去不相等的字符,从这里开始按照 dp 那样转移即可,具体可见代码。为了提升效率加一条最优性剪枝,因为当前答案一定不小于目前搜索的答案加上需要处理的字符串的长度之差(必须用加减字符达成同样长度)。\(O(n ^ 2)\) 暴力枚举一下搜就好了。

我说,有一个小细节,string 的长度是 size_t 类型的,如果减的话可能会爆炸,要强转 int。

我说,这里算的是起点,dfs(x, y, step) 表示字符串 \(s[x \dots \operatorname{end}]\)\(t[y \dots \operatorname{end}]\),目前耗费了 \(step\) 步,而 dp 算的是一端的终点,本质相同。

namespace liuzimingc {
const int N = 205;
#define endl '\n'

int n, i, j, res, ans[10];
string s[N];

void dfs(int x, int y, int step) {
	if (step + abs((int)(s[i].length() - x) - (int)(s[j].length() - y)) >= res) return;
	while (x < s[i].length() && y < s[j].length() && s[i][x] == s[j][y]) x++, y++;
	if (x >= s[i].length() && y >= s[j].length()) {
		res = step;
		return;
	}
	if (x < s[i].length()) dfs(x + 1, y, step + 1);
	if (y < s[j].length()) dfs(x, y + 1, step + 1);
	if (x < s[i].length() && y < s[j].length()) dfs(x + 1, y + 1, step + 1);
}

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> s[i];
	for (i = 1; i <= n; i++)
		for (j = i + 1; j <= n; j++) {
			res = 9;
			dfs(0, 0, 0);
			ans[res]++;
		}
	for (int i = 1; i <= 8; i++) cout << ans[i] << " \n"[i == 8];
	return 0;
}
#undef int
} // namespace liuzimingc

我说,我不会傻逼爆搜。说无可说。

消防

首先,枢纽一定在树的直径上。

证明咕咕。

那么我们随便拉出来一条直径,那么所有点到路径的距离就分成两种情况:

  • 点在直径上。只能是路径的两个端点。可以用双指针 \(O(n)\) 维护长度和不大于 \(s\) 的路径(显然越大越好)。显然一定是直径两端离路径最远,\(O(1)\) 算就行。
  • 点不在直径上。从直径上的点对其他点 dfs 一下就好了,每个点只会遍历一次所以是 \(O(n)\) 的。

然后综合两种情况就做完了!

namespace liuzimingc {
const int N = 3e5 + 5;
#define endl '\n'

int n, s, dep[N], x, fa[N], ans = 0x3f3f3f3f;
vector<pair<int, int>> e[N];
bool vis[N];

void dfs(int u, int f) {
	fa[u] = f;
	if (dep[u] > dep[x]) x = u;
	for (const auto &i : e[u]) {
		int v = i.first, w = i.second;
		if (v == f || vis[v]) continue;
		dep[v] = dep[u] + w;
		dfs(v, u);
	}
} // dep 得到从某一点开始到所有点的距离 

int main() {
	ios::sync_with_stdio(false);
	cin.tie(nullptr); cout.tie(nullptr);
	cin >> n >> s;
	for (int i = 1; i < n; i++) {
		int u, v, w;
		cin >> u >> v >> w;
		e[u].push_back(make_pair(v, w));
		e[v].push_back(make_pair(u, w));
	}
	x = 1;
	dfs(1, 0);
	dep[x] = 0;
	dfs(x, 0); // 两次都找距离最远的点,求直径
	int begin = x; // 这条直径的最底端(抽象说法),fa[x] 就是跳上去
	for (int i = begin, j = begin; i; i = fa[i]) {
		while (dep[j] - dep[i] > s) j = fa[j];
		ans = min(ans, max(dep[i], dep[begin] - dep[j])); 
	}
	for (int i = begin; i; i = fa[i]) vis[i] = true;
	for (int i = begin; i; i = fa[i]) {
		x = i;
		dep[i] = 0;
		dfs(i, fa[i]);
	}
	for (int i = 1; i <= n; i++) ans = max(ans, dep[i]); // 两种情况都要满足
	cout << ans << endl;
	return 0;
}
#undef int
} // namespace liuzimingc

以及顺便学到了两次 bfs / dfs 求直径,以前不知道。。。

小凸玩密室

每一次花费和上一次选的点有关,直接记录显然死翘翘,感觉很辣手,不好操作。

在点灯的过程中,要保证任意时刻所有被点亮的灯泡必须连通,在点亮一个灯泡后必须先点亮其子树所有灯泡才能点亮其他灯泡。

因为是二叉树,相当于点亮一个灯泡之后只会要么点左儿子要么点右儿子(如果有),直到其子树被点完。然后考虑整体的过程,点完一颗子树后,肯定需要回退到那个点的祖先上去,然后再选另一颗子树(如果有),如此重复操作。那么我们根据这个来定义状态,\(f_{i, j, 0}\) 表示从 \(i\) 本身出发并点亮其除去 \(i\) 的子树到 \(i\)\(j\) 级祖先并点亮它的最小代价,\(f_{i, j, 1}\) 表示从从 \(i\) 本身出发并点亮其除去 \(i\) 的子树到 \(i\)\(j\) 级祖先的另一个儿子并点亮它的最小代价。注意定义 \(i\) 这里没有包含,而点儿子、祖先包含了,是为了良好的顺接起来整个过程,否则如果都包含会出现重复。

那么定义完之后其实状态转移就比较 OK 了。比如若 \(i\) 有左右儿子,计算 \(f_{i, j, 1}\),则分成两种情况:先点左子树或先点右子树,这里又以先点左子树为例,那么先点左儿子,然后点左儿子的子树除去左儿子并回到原节点,然后点右儿子,最后点右儿子的子树除去右儿子并回到 \(i\)\(j\) 级祖先

龙门对决

咕咕。