2025-12-10 hetao1733837的刷题笔记
LG2664 树上游戏
原题链接:树上游戏
分析
这咋点分治?
显然,这个需要一定的转化,因为普通点分治是不好合并两个子树的 c n t cnt cnt 的。
那么,换一个角度思考,既然直接求不好求,那我对于每个颜色 j j j,记 c n t j cnt_j cntj 表示以 i i i 为端点,包含颜色 j j j 的路径数量,那么 s u m i = ∑ c n t j sum_i=\sum cnt_j sumi=∑cntj。 c n t j cnt_j cntj 的更新也很容易, c n t c o l u + = s z u cnt_{col_u}+=sz_u cntcolu+=szu, u u u 是子树。
行,那我们维护子树中以当前根节点为端点的路径对根的贡献 和lca 为当前根节点的路径对子树内每个点的贡献,第一部分好做,第二部分,分讨出现的颜色即可。
正解
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 200005;
int n, tot, sz[N], mx, rt;
int c[N];
bool vis[N];
vector<int> e[N];
void getrt(int u, int fa){
sz[u] = 1;
int maxn = 0;
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
getrt(v, u);
sz[u] += sz[v];
maxn = max(maxn, sz[v]);
}
maxn = max(maxn, tot - sz[u]);
if (maxn < mx){
mx = maxn;
rt = u;
}
}
int ans[N], sum;
int cnt[N], val[N];
int root;
void getdis(int u, int fa, int col){
sz[u] = 1;
if (!val[c[u]]){
sum -= cnt[c[u]];
col++;
}
val[c[u]]++;
ans[u] += sum + col * sz[root];
for (auto v : e[u]){
if (v == fa || vis[v]){
continue;
}
getdis(v, u, col);
sz[u] += sz[v];
}
val[c[u]]--;
if (!val[c[u]]){
sum += cnt[c[u]];
}
}
void calc(int u, int fa){
if (!val[c[u]]){
cnt[c[u]] += sz[u];
sum += sz[u];
}
val[c[u]]++;
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
calc(v, u);
}
val[c[u]]--;
}
void clear1(int u, int fa, int col){
if (!val[c[u]])
col++;
val[c[u]]++;
ans[u] -= col;
ans[root] += col;
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
clear1(v, u, col);
}
val[c[u]]--;
}
void clear2(int u, int fa){
cnt[c[u]] = 0;
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
clear2(v, u);
}
}
int son[N];
void solve(int u){
vis[u] = 1;
int total = 0;
root = u;
ans[u]++;
for (auto v : e[u]){
if (vis[v])
continue;
son[++total] = v;
}
sz[u] = 1;
sum = 1;
cnt[c[u]] = 1;
val[c[u]] = 1;
for (int i = 1; i <= total; i++){
int v = son[i];
getdis(v, u, 0);
calc(v, u);
sz[u] += sz[v];
cnt[c[u]] += sz[v];
sum += sz[v];
}
clear2(u, 0);
sz[u] = 1;
sum = 1;
cnt[c[u]] = 1;
val[c[u]] = 1;
for (int i = total; i >= 1; i--){
int v = son[i];
getdis(v, u, 0);
calc(v, u);
sz[u] += sz[v];
cnt[c[u]] += sz[v];
sum += sz[v];
}
val[c[u]] = 0;
clear1(u, 0, 0);
clear2(u, 0);
for (auto v : e[u]){
if (vis[v])
continue;
tot = sz[v];
mx = n + 1;
rt = 0;
getrt(v, u);
solve(rt);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++){
cin >> c[i];
}
for (int i = 1; i < n; i++){
int x, y;
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
rt = 0;
tot = n;
mx = n + 1;
getrt(1, 0);
solve(rt);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
}
LG6626 [省选联考 2020 B 卷] 消息传递
原题链接:[省选联考 2020 B 卷] 消息传递
分析
我的思路并未将我引向点分治,而是引向了猜结论......好像这种类似传染的维护是最多不超过某个瓶颈的。
神秘转化,那不就是与给定点距离等于 k k k 的点的个数?
行,那就可以了,开始he!
问题来了,如何控制某一个点?难道把该点强制作为整棵树的根?那不就是深度问题了?并非可以,因为这样是 O ( m n ) O(mn) O(mn) 的。
行吧,我们看点分治怎么操作。
由上面暴力的思想,我们不妨将其离线。然后直接做就行。
正解
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int T, n, m, tot, rt, sz[N], mx, ans[N], de[N], buc[N], d;
vector<int> e[N];
bool vis[N];
vector<pair<int, int>> ask[N], tmp;
void getrt(int u, int fa){
sz[u] = 1;
int maxn = 0;
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
getrt(v, u);
sz[u] += sz[v];
maxn = max(maxn, sz[v]);
}
maxn = max(maxn, tot - sz[u]);
if (maxn < mx){
mx = maxn;
rt = u;
}
}
void getdis(int u, int fa){
d = max(d, de[u]);
buc[de[u]]++;
for (auto o : ask[u]){
if (o.first >= de[u])
tmp.push_back({o.first - de[u], o.second});
}
for (auto v : e[u]){
if (v == fa || vis[v])
continue;
de[v] = de[u] + 1;
getdis(v, u);
}
}
void calc(int u){
tmp.clear();
d = 0;
de[u] = 0;
getdis(u, 0);
for (auto o : tmp){
ans[o.second] += buc[o.first];
}
for (int i = 0; i <= d; i++){
buc[i] = 0;
}
for (auto v : e[u]){
if (vis[v])
continue;
de[v] = 1;
tmp.clear();
d = 0;
getdis(v, u);
for (auto o : tmp)
ans[o.second] -= buc[o.first];
for (int i = 0; i <= d; i++)
buc[i] = 0;
}
}
void solve(int u){
calc(u);
vis[u] = 1;
for (auto v : e[u]){
if (vis[v])
continue;
tot = sz[v];
rt = 0;
mx = 0x3f3f3f3f;
getrt(v, u);
solve(rt);
}
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> T;
while (T--){
cin >> n >> m;
for (int i = 1; i <= n; i++){
vis[i] = 0;
ask[i].clear();
e[i].clear();
}
for (int i = 1; i <= m; i++){
ans[i] = 0;
}
for (int i = 1; i < n; i++){
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
for (int i = 1; i <= m; i++){
int x, k;
cin >> x >> k;
ask[x].push_back({k, i});
}
tot = n;
rt = 0;
mx = 0x3f3f3f3f;
getrt(1, 0);
solve(rt);
for (int i = 1; i <= m; i++)
cout << ans[i] << '\n';
}
}
LG2056 [ZJOI2007] 捉迷藏
原题链接:[ZJOI2007] 捉迷藏
分析
很明显,是一个动态问题。
点分树这个思想🐂🍺啊,他相当于把分治的过程建了树,然后修改变成了树上的 l o g log log,🐂🍺!
那么,对于本题,我们发现,在点分树上,最长距离一定不在同一棵子树,所以,记录每棵子树中黑点到子树根的最长距离即可。那么,对于改成黑点,相当于点分树删除;改成白点,相当于点分树加点。
为了快速得出最值,我们开 p r i o r i t y _ q u e u e priority\_queue priority_queue 即可!
正解
没一个能过 hack 的!****************************************************************************************************************
不写了!
LG6329 【模板】点分树 / 震波
原题链接:【模板】点分树 / 震波
分析
放弃了!
正解
放弃了!
我要清一清洛谷首页!
暴力判断把 P1101 单词方阵 给过了,我以前这么菜吗?
LG5838 [USACO19DEC] Milk Visits G
原题链接:[USACO19DEC] Milk Visits G
分析
我必须给这个树剖鼓掌,他把每种 c c c 的 d f n dfn dfn 扔进一个 b u c buc buc,然后在路径查找时二分,虽然是双 l o g log log,但是我"会"树剖这个 t r i c k trick trick 是天才的。甚至省略了线段树!!!
正解
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, m, t[N];
vector<int> e[N], buc[N];
int fa[N], son[N], top[N], sz[N], de[N];
void dfs1(int u, int pa){
sz[u] = 1;
fa[u] = pa;
de[u] = de[pa] + 1;
for (auto v : e[u]){
if (v == pa)
continue;
dfs1(v, u);
sz[u] += sz[v];
if (sz[v] > sz[son[u]])
son[u] = v;
}
}
int dfn[N], rk[N], ncnt;
void dfs2(int u, int tp){
top[u] = tp;
dfn[u] = ++ncnt;
rk[ncnt] = u;
if (son[u])
dfs2(son[u], tp);
for (auto v : e[u]){
if (v == fa[u] || v == son[u])
continue;
dfs2(v, v);
}
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n >> m;
for (int i = 1; i <= n; i++){
cin >> t[i];
}
for (int i = 1; i < n; i++){
int x, y;
cin >> x >> y;
e[x].push_back(y);
e[y].push_back(x);
}
dfs1(1, 0);
dfs2(1, 1);
for (int i = 1; i <= ncnt; i++){
buc[t[rk[i]]].push_back(i);
}
while (m--){
int a, b, c;
cin >> a >> b >> c;
bool flag = 0;
while (top[a] != top[b]){
if (de[top[a]] < de[top[b]])
swap(a, b);
auto it = lower_bound(buc[c].begin(), buc[c].end(), dfn[top[a]]);
if (it != buc[c].end() && *it <= dfn[a]){
flag = 1;
break;
}
a = fa[top[a]];
}
if (!flag){
if (dfn[a] > dfn[b])
swap(a, b);
auto it = lower_bound(buc[c].begin(), buc[c].end(), dfn[a]);
if (it != buc[c].end() && *it <= dfn[b])
flag = 1;
}
cout << flag;
}
}
LG1637 三元上升子序列
原题链接:三元上升子序列
分析
树状数组+动态规划!国庆的时候 Taoran 就讲过,行,作为第一道来写!
正解
cpp
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 30005, M = 5;
int n, a[N], b[N], m, dp[N][M], c[N];
void add(int x, int v){
for (int i = x; i <= n; i += i & (-i))
c[i] += v;
}
int query(int x){
int res = 0;
for (int i = x; i; i -= i & (-i))
res += c[i];
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
cin >> n;
for (int i = 1; i <= n; i++){
cin >> a[i];
b[i] = a[i];
}
sort(b + 1, b + n + 1);
m = unique(b + 1, b + n + 1) - b - 1;
for (int i = 1; i <= n; i++){
dp[i][1] = 1;
a[i] = lower_bound(b + 1, b + m + 1, a[i]) - b;
}
for (int i = 2; i <= 3; i++){
memset(c, 0, sizeof(c));
for (int j = 1; j <= n; j++){
dp[j][i] = query(a[j] - 1);
add(a[j], dp[j][i - 1]);
}
}
int ans = 0;
for (int i = 1; i <= n; i++)
ans += dp[i][3];
cout << ans;
}
感觉本质还是树状数组对于逆序对求解中的一些思想。