2025-11-04 hetao1733837的刷题记录

Ⅰ.P3914 染色计数

原题链接:P3914 染色计数 - 洛谷

分析

多限制了颜色,则将其写进状态。设f_{u,i}表示u子树,u节点强制染成i颜色的总方案数。

考虑转移,利用乘法原理,我们可以轻松得出:

f_{u,i}=\prod_{v\in son_u}^{}(\sum_{j =1,j\neq i}^{m}f_{v,j}) \pmod{10^9+7}

正解

#include <bits/stdc++.h>
#define ll long long
#define P 1000000007
using namespace std;
const int N = 5005;
int n, m, k, u, v, cnt, f[N][N];
ll S[N];
vector<int> e[N];
void dfs(int u, int fa){
    for (auto v : e[u]){
        if (v == fa)
            continue;
        dfs(v, u);
        for (int i = 1; i <= m; i++){
            f[u][i] = f[u][i] * (S[v] - f[v][i]) % P;
        }
    }
    for (int i = 1; i <= m; i++)
        S[u] = (S[u] + f[u][i]) % P;
}
int main(){
    cin >> n >> m;
    for (int i = 1; i <= n; i++){
        cin >> cnt;
        for (int j = 1; j <= cnt; j++){
            cin >> k;
            f[i][k] = 1;
        }
    }
    for (int i = 1; i < n; i++){
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    dfs(1, 0);
    cout << (S[1] + P) % P; // 避免负数
}

Ⅱ.P13020 [GESP202506 八级] 遍历计数

原题链接:P13020 [GESP202506 八级] 遍历计数 - 洛谷

分析

f_u表示u子树内的dfn数量,更新为:f_u=cnt[son_u]!\times\prod _{v\in son_u}f_v

初始化,f_u=1

答案,f_1

但是,复杂度是O(n^2)的。

我们分析一下过程,发现每个点对答案的贡献就是它子节点数量的阶乘。根节点子节点数量就是其度数,其余节点是度数-1。设根节点为rtd_i为各节点的度数,那么ans=d_{rt}!\times\prod _{i\in [1,n],i\neq rt}(d_i - 1)!

然后预处理前缀积Pre以及后缀积Suf,ans=d_{rt}!\times Pre_{rt - 1}\times Suf_{rt+1}

正解

#include <bits/stdc++.h>
#define mod 100000000
using namespace std;
const int N = 100005;
int n, u, v;
int d[N];
long long Pre[N], Suf[N], fac[N];
int main(){
    cin >> n;
    for (int i = 1; i < n; i++){
        cin >> u >> v;
        d[u]++;
        d[v]++;
    }
    fac[0] = 1;
    for (int i = 1; i <= n; i++){
        fac[i] = fac[i - 1] * i % mod;
    }
    Pre[0] = 1;
    for (int i = 1; i <= n; i++){
        Pre[i] = Pre[i - 1] * fac[d[i] - 1] % mod;
    }
    Suf[n + 1] = 1;
    for (int i = n; i >= 1; i--){
        Suf[i] = Suf[i + 1] * fac[d[i] - 1] % mod;
    }
    long long ans = 0;
    for (int u = 1; u <= n; u++)
        ans = (ans + Pre[u - 1] * Suf[u + 1] % mod * fac[d[u]]) % mod;
    cout << ans;
} 

Ⅲ.AT_abc259_f [ABC259F] Select Edges

原题链接1:AT_abc259_f [ABC259F] Select Edges - 洛谷

原题链接2:F - Select Edges

分析

学到这其实是很自然的了,设f_{u,i}表示u子树,选择了i条与u相连的边的最大边权和。

但是,u到u的父亲还有一条边,我们考虑到加上这条边只会让i增加1,那么我们就可以简化状态,我们设f_{u,0/1}表示u子树,已选边小于/小于等于d_u的最大边权和。

考虑转移,

①不选边(u,v):v子树贡献f_{v, 1}

②选边(u,v):v子树贡献w(u,v)+f[v][0]

实现

先跑①,在更新②

正解

#include <bits/stdc++.h>
using namespace std;
const int N = 300005;
int n, d[N], u, v, w;
long long f[N][2];
vector<pair<int, int>> e[N];
void dfs(int u, int fa){
    vector<long long> b;
    for (auto tmp : e[u]){
        int v = tmp.first, w = tmp.second;
        if (v == fa)
            continue;
        dfs(v, u);
        f[u][0] += f[v][1];
        b.push_back(w + f[v][0] - f[v][1]);
    }
    f[u][1] = f[u][0];
    sort(b.begin(), b.end(), greater<long long>());
    int cnt = 0;
    for (auto x : b){
        ++cnt;
        if (cnt < d[u] && x > 0)
            f[u][0] += x;
        if (cnt <= d[u] && x > 0)
            f[u][1] += x;
    }
    if (d[u] == 0)
        f[u][0] = 0xc0c0c0c0c0c0c0c0;
}
int main(){
    cin >> n;
    for (int i = 1; i <= n; i++)
        cin >> d[i];
    for (int i = 1; i < n; i++){
        cin >> u >> v >> w;
        e[u].push_back({v, w});
        e[v].push_back({u, w});
    }
    dfs(1, 0);
    cout << f[1][1];
}

树形DP一般设计成f_{u,state}的形式,u表示u子树内,state表示多余限制

Ⅳ.P3174 [HAOI2009] 毛毛虫

原题链接:P3174 [HAOI2009] 毛毛虫 - 洛谷

分析

题目形式化为对于一棵树,找到一条链使得链上节点及其相邻节点总和最大。

那么,我们设f_u表示以u子树内以u为一端点的最大毛毛虫的大小,

考虑转移,f_u=mx1+e[u].size()+1-1-1(加u本身,减v_1和u父亲,其中mx1是最大的儿子)。

那么u节点处的答案就是max(mx1+e[u].size(),mx1+mx2+e[u].size()- 1)(其中,mx1和mx2分别是最大值和次大值)

似懂非懂……

正解

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 300005;
int n, m, k, s, t, f[N], ans = 1;
vector<int> e[N];
void dfs(int u, int fa){
    if (u != 1 && e[u].size() == 1){ //特判叶子
        f[u] = 1;
        return ;
    }
    int mx1 = 0, mx2 = 0;
    for (auto v : e[u]){
        if (v == fa)
            continue;
        dfs(v ,u);
        if (f[v] >= mx1){
            mx2 = mx1;
            mx1 = f[v];
        }
        else if (f[v] > mx2)
            mx2 = f[v];
    }
    ans = max({ans, mx1 + (int)e[u].size(), mx1 + mx2 + (int)e[u].size() - 1});
    f[u] = mx1 + e[u].size() - 1;
}
int main(){
    cin >> n >> m;
    for (int i = 1; i <= m; ++i){
        cin >> s >> t;
        e[s].push_back(t);
        e[t].push_back(s);
    }
    dfs(1, 0);
    cout << ans;
}

Ⅴ.P6147 [USACO20FEB] Delegation G

原题链接:P6147 [USACO20FEB] Delegation G - 洛谷

分析

我们发现,只有N-1\equiv 0\pmod K时才成立。那么,最多有2\sqrt{N-1}中可能。

f_u表示u子树内已经存在了若干长度为K的链,剩余一条以u为端点的链的长度(可证长度确定)。

实现

1.先遍历u的所有子节点找出剩余的不能组合成K的链,尽可能配对

2.再遍历u的所有子节点看是否最多剩余一条链(若超过1条就无解了)

正解

#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int N = 100005;
const int mod = 1e9 + 7;
int n, u, v, K, ans, f[N], cnt[N]; // f[u]:u子树正常划分后,当前链长度
vector<int> e[N];
void dfs(int u, int fa){
    if (u != 1 && e[u].size() == 1){ // 特判叶节点
        f[u] = 0;
        return;
    }
    for (auto v : e[u]){
        if (v == fa) continue;
        dfs(v, u); // 计算f[v] 
    }
    for (auto v : e[u]){
        if (v == fa || f[v] + 1 == K)
            continue;
        if (cnt[K - (f[v] + 1)]) //和当前链匹配的长度存在
            --cnt[K - (f[v] + 1)];
        else
            ++cnt[f[v] + 1];
    }
    f[u] = 0;
    for (auto v : e[u]){
        if (v == fa || f[v] + 1 == K)
            continue;
        if (cnt[f[v] + 1] == 1){ //发现一条链
            if (f[u])
                ans = 0; //之前有别的剩余链,无解
            f[u] = f[v] + 1; //更新剩余链长度
            --cnt[f[v] + 1]; //从桶中移除该链的计数,避免重复处理
        }
        if (cnt[f[v] + 1] > 1) //不止一条该长度剩余链
            ans = 0; //无解
    }
}
int main(){
    cin >> n;
    for (int i = 1; i < n; ++i){
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }
    cout << 1;
    for (int i = 2; i <= n - 1; ++i)
        if ((n - 1) % i == 0){
            memset(cnt, 0, sizeof(cnt));
            K = i; ans = 1;
            dfs(1, 0);
            cout << (ans && (f[1] == 0));//可行对应ans等于1并且f[1]等于0 
        }
        else cout << 0;
}

Ⅵ.P6142 [USACO20FEB] Delegation P

原题链接:P6142 [USACO20FEB] Delegation P - 洛谷

分析

区别于上一题的恰好是K,这题要求至少是K,那么我们看似失去了一些约束,但是考虑到K越小似乎越容易满足!那就二分答案!

正解

#include <bits/stdc++.h>
using namespace std;
const int N = 100005;
int n, ans, res, f[N], K; // f[u] 记录节点u向上传递的未匹配的链长度,res 标记是否可行,K 是当前二分的中间值
vector<int> e[N]; // 邻接表存储树的结构
// 深度优先搜索函数,u是当前节点,fa是父节点
void dfs(int u, int fa){
	multiset<int> ms; // 用于存储子节点向上传递的链长度
	for (auto v : e[u]){ // 遍历当前节点的所有子节点
		if (v == fa) continue; // 跳过父节点
		dfs(v, u); // 递归处理子节点
		ms.insert(f[v] + 1); // 将子节点向上传递的链长度加1后插入集合
	}
	f[u] = 0; // 初始化当前节点向上传递的链长度为0
	// 如果当前节点不是根节点且子节点数量为偶数,插入0(用于处理匹配)
	if (u != 1 && ms.size() % 2 == 0) 
        ms.insert(0);
	while (!ms.empty()){ // 当集合不为空时进行匹配
		int x = *ms.begin(); // 取出集合中的最小值
		ms.erase(ms.begin()); // 删除该最小值
		// 二分查找集合中是否存在大于等于K - x的元素
		auto y = ms.lower_bound(K - x);
		if (y == ms.end()){ // 如果不存在这样的元素
			if (f[u] > 0){ // 如果当前节点已经有未匹配的链长度
				res = 0; // 标记为不可行
				return;
			}
			f[u] = x; // 将x作为当前节点向上传递的未匹配链长度
		} 
        else{
			ms.erase(y); // 存在则删除该元素,完成匹配
		}
	}
}
// 检查函数,判断是否可以划分出每条链长度至少为mid的链
bool check(int mid){
	res = 1; // 初始化为可行
	K = mid; // 设置当前检查的长度
	dfs(1, 0); // 从根节点开始深度优先搜索
	// 如果可行且根节点向上传递的链长度为0或者大于等于K,返回true
	return (res == 1 && (f[1] == 0 || f[1] >= K));
}
int main(){
	cin >> n; // 输入节点数量
	for (int i = 1; i < n; i++){ // 输入n - 1条边
		int u, v;
		cin >> u >> v;
		e[u].push_back(v); // 建立邻接表
		e[v].push_back(u);
	}
	int l = 1, r = n - 1, mid; // 二分查找的左右边界
	while (l <= r){ // 二分查找过程
		mid = (l + r) / 2;
		if (check(mid)){ // 如果当前mid可行
			ans = mid; // 更新答案
			l = mid + 1; // 尝试更大的长度
		} 
        else{
			r = mid - 1; // 尝试更小的长度
		}
	}
	cout << ans; // 输出最大的K
	return 0;
}

路径相关的树形DP,看成直上直下的链!再合并!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值