DP 好题记录

Armor and Weapons

\(f_{i, j}\) 是盔甲 \(i\),武器 \(j\) 时的最少时间,转移是平凡的。但是 \(O(n^2)\) 显然超时。

注意答案不会太大,因为当没有加成时,在 \(i\) 轮的情况下最多可以得到 \(Fib_{i + 2}\) 的盔甲或武器。由此可知答案大概在 \(O(\log n)\) 的量级。

于是我们考虑换量:设 \(f_{i, j}\) 是在通过 \(i\) 轮,盔甲为 \(j\) 时,我们最大能拿到什么武器。

初始化 \(f_{1, 1} = 1\)。转移容易。

qwq
#include<bits/stdc++.h>
#define int long long 
using namespace std; 

const int N = 2e5 + 10;

int n, m, t;
map<pair<int, int>, int> EX;
int f[N], g[N];

int val(int i){
	int res = i + g[i];
	if(EX.count({i, g[i]})) res++;
	return res; 
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	bool swp = false;
	cin >> n >> m >> t; if(n > m) swap(n, m), swp = true;
	for(int i = 1; i <= t; i++){
		int x, y; cin >> x >> y; if(swp) swap(x, y);
		EX[make_pair(x, y)] = 1;
	}
	int ans = 0; g[1] = 1;
	if(n == 1 && m == 1){cout << 0; return 0;}
	while(++ans){
		for(int i = 1; i <= n; i++) if(g[i]) f[i] = max(f[i], min(m, val(i))), f[min(n, val(i))] = max(f[min(n, val(i))], g[i]); 
		for(int i = n; i; i--) g[i] = max(g[i + 1], f[i]), f[i] = 0;
//		cout << g[n] << "\n";
		if(g[n] >= m){cout << ans; return 0;} 
	}

	return 0;
}

Formalism for Formalism

首先考虑转化问题,我们称一个等价集合是一个集合元素全部等价的集合,问题转化成求集合数量。

但这样还不好做,于是我们考虑用一个集合中字典序最小的那个字符串来代替该集合,这是常见的 \(\rm Trick\)

考虑从前往后填数,现在考虑第 \(i\) 位填什么数。由于我们填出来的数有字典序最小的限制,在某些状态下,有一些数我们可能是不能填的,但是我们并不知道我们可以填些什么数。

注意到可能填的数只有 \(10\) 种,我们可以直接将可以填的数加入到状态表示中。自然的,我们令 \(f_{i, S}\) 为当填到第 \(i\) 位,下一位填数可行状态集表示为 \(S\) 时,最大数组长度为多少。

找到 \(f_{i, S}\) 的前驱是困难的,于是我们考虑 “我为人人” 的刷表法。对于一个 \(f_{i - 1, S}\) 这样的已知状态,我们用它来为 \(f_{i, S0}\) 这样的状态做贡献。我们枚举第 \(i\) 位填的数,现在来计算 \(S0\)。容易发现,第 \(i + 1\) 位不可填数 \(j\),当且仅当 \(j\)\(i\) 可以交换且 \(j < i\)

于是我们预处理出每种状态的后继即可。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;
 
const int N = 1e5 + 10, V = 10, mod = 998244353;
int n, m, nxt[(1 << V + 2)][V + 2], g[(1 << V + 2)], f[(1 << V + 2)];
bool con[V + 2][V + 2];
 
signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n >> m;
	for(int i = 1; i <= m; i++){
		int x, y; cin >> x >> y;
		con[x][y] = con[y][x] = true;
	}
	g[0] = 1;
	for(int S = 0; S < (1 << V); S++){
		for(int i = 0; i < V; i++){
			if(S & (1 << i)){nxt[S][i] = -1; continue;}
			for(int j = 0; j < V; j++) if(con[i][j] && (j < i || (S & (1 << j)))) nxt[S][i] |= (1 << j);
		}
	}
	for(int i = 1; i <= n; i++){
		for(int S = 0; S < (1 << V); S++){
			if(!g[S]) continue; //这个优化很重要!
			for(int ths = 0; ths < V; ths++){
				if(nxt[S][ths] != -1) f[nxt[S][ths]] = (f[nxt[S][ths]] + g[S]) % mod;
			}
		}
		for(int i = 0; i < (1 << V); i++) g[i] = f[i], f[i] = 0;
	}
	int ans = 0;		
	for(int i = 0; i < (1 << V); i++) ans = (ans + g[i]) % mod;
	cout << ans;
 
// 	system("pause");
	return 0;
}

P3643 [APIO2016] 划艇

首先容易设计出朴素 DP:设 \(f_{i, j}\) 为考虑前 \(i\) 个数,且 \(a_i = j\) 时的方案数。

但是 \(j\) 可能很大,于是考虑优化状态个数。

如何优化呢?注意到题目中数值上限有 \(10^9\),但是出现的数值只有 \(2n\) 个,进而我们考虑离散化。注意,我们为了方便处理,将每个 \(r_i\) 都加一,再进行离散化。

令第 \(i\) 小的数对应原来的数是 \(rk_i\),不妨设有 \(m\) 个不同的数。显然的,我们将 \(rk_1 ~ rk_m\) 分为了 \(m - 1\) 个互不相交的区间。注意到一个原来的区间一定可以通过几个相邻的区间组合而成,从而可以优化状态个数:

\(f_{i, j}\) 为考虑前 \(i\) 个数,且 \(a_i \in [rk_j, rk_{j + 1})\) 的方案数。于是我们枚举它的前驱 \(lst\),并且让 \([lst + 1, i]\) 区间中的任意 \(a_k\)\(\in [rk_j, rk{j + 1})\) 或等于 \(0\)(我们称不选为令 \(a_k = 0\))。

但是区间中可能会有一些 \([l_k, r_k] \notin [rk_j, rk_{j + 1})\),我们只能强制 \(a_k = 0\),这些不需要考虑。我们只考虑剩下的区间,不妨设剩下的区间有 \(cnt\) 个。(接下来区间长度 \(len = rk_{j + 1 - rk_j}\)

单独考虑是困难的,于是我们考虑对它们整体考虑。首先分析没有 \(0\) 的情况,可以发现如果我们选了 \(len\) 个不同数中的 \(i - lst\) 个,序列排列方式将是唯一的,所以答案是 \(C_{len, i - lst + 1}\)。接下来在序列中加入 \(i - lst\) 个零(因为 \(a_i\) 不能为 \(0\))。对他们进行标号 \(1....i-lst\),对于一个标号为 \(i\)\(0\),我们将它放置在第 \(i\) 个位置。于是其实对于一个不同的选择集合,不难发现都有唯一的方式,于是有 \(C_{len + cnt - 1}^{cnt}\) 放置方式。

从而我们可以得出状态转移方程:

\[f_{i, j} = \sum^{i-1}_{lst = 0}{\sum^{j - 1}_{j' = 0} f_{lst, j'} \times C_{len + cnt - 1}^{cnt}} \]

初始化 \(f_{0, 0} = 1\),答案 \(\sum_{i = 1} ^ n {f_{i, m - 1}}\)

但这样是 \(O(n^4)\) 的,容易发现可以用前缀和优化。具体的,我们令 \(s_{i, j} = \sum_{j' = 0}^j {f_{i, j'}}\)。状态转移方程就为 \(f_{i, j} = \sum_{lst = 1}^i s_{lst, j - 1} \times C_{len + cnt - 1}^{cnt}\)。但是这样又有一个问题,C 的计算不是容易的。注意到 \(C(x, y) = C(x - 1, y - 1) \cdot x \cdot inv(y)\),而且当 \(cnt = 1\) 时,这是容易计算的,于是我们可以边计算 \(f\) 边更新 \(C\) 的值。

qwq
#include<bits/stdc++.h>
#define int long long
using namespace std;

const int N = 500 + 10, mod = 1e9 + 7;

int n, m, f[N][2 * N], l[N], r[N], rk[2 * N], s[N][2 * N], C[N];

int qpow(int x, int y){
	int ret = 1;
	while(y){
		if(y & 1) ret = ret * x % mod;
		x = x * x % mod;
		y >>= 1;
	}
	return ret;
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i <= n; i++) cin >> l[i] >> r[i], r[i]++, rk[i] = l[i], rk[i + n] = r[i];
	sort(rk + 1, rk + 2 * n + 1); m = unique(rk + 1, rk + 2 * n + 1) - (rk + 1);
	for(int i = 1; i <= n; i++) l[i] = lower_bound(rk + 1, rk + m + 1, l[i]) - rk, r[i] = lower_bound(rk + 1, rk + m + 1, r[i]) - rk;	
	for(int i = 0; i < m; i++) s[0][i] = 1;
	for(int j = 1; j < m; j++){
		int len = rk[j + 1] - rk[j], tot = 0; C[0] = len;
		for(int i = 1; i <= n; i++) C[i] = (C[i - 1] * (i + len) % mod) * qpow(i + 1, mod - 2) % mod;
		for(int i = 1; i <= n; i++){
			tot = 0;
			if(l[i] <= j && r[i] > j){
				for(int lst = i - 1; lst >= 0; lst--){
					f[i][j] = (f[i][j] + s[lst][j - 1] * C[tot] % mod) % mod;
					if(l[lst] <= j && r[lst] > j) tot++;
				} 
			}
			s[i][j] = (s[i][j - 1] + f[i][j]) % mod;
		}
	}
	int ans = 0;
	for(int i = 1; i <= n; i++) ans = (ans + s[i][m - 1]) % mod;
	cout << ans;
// 	system("pause");
	return 0;
}
/*
f[i][j] : 前 i 个数,a[i] 在 [j, j + 1)
枚举使 [lst + 1, j] 都在 [rk[j], rk[j + 1]) 的 lst(0 <= lst < i)
f[i][j] = \sum{s[lst][j - 1] * C(rk[j] - rk[j - 1] + cnt - 1, cnt)}
初始化:f[0][0] = 1
C(rk[j + 1] - rk[j], 1) = rk[j + 1] - rk[j]
C(x + 1, y + 1) = jc[x + 1] * jcinv[y + 1] * jcinv[x - y]
C(x, y) = jc[x] * jcinv[y] * jcinv[x - y]
C(x + 1, y + 1) = C(x, y) * (x + 1) * inv[y + 1]
*/2024-07-06 21:45:00 星期六

Interesting Problem Easy/Hard

首先给出一个关键但很显然的性质:一个决策只被前面的决策影响。于是我们可以设计一个区间 \(\rm DP\):设 \(f_{l, r, k}\) 为若 \([1, l - 1]\) 进行了 \(k\) 次操作, 区间 \([l, r]\) 最多可以进行多少操作。转移有两种:

  • 消掉 \([l + 1, r - 1]\) 后再消 \((l, r)\)

  • 找到一个断点 \(k\),先消 \([l, k]\) 再消 \([k + 1, r]\)(注意这里的先后顺序是不固定的,即假如 \([l, k]\) 的可消除对数大于 \([k + 1, r]\) 所需要的消除对数 \(x\),可以通过先操作 \([l, k]\) \(x\) 次后在再进行 \([k + 1, r]\) 的消除操作)。

时间复杂度 \(O(n^4)\),可以通过 \(\rm Easy\)

优化是比较困难的,考虑进一步发掘题目中的性质。观察到答案一定是由一段段全部消除的区间拼接而成的。受到上面的启发,我们可以再次设计出一个 \(\rm DP\)\(f_{l, r}\) 是将区间 \([l, r]\) 全部消除最少所需要 \([1, l]\) 消除多少对。转移是容易的。如何计算答案呢?再设计一个 \(\rm DP\) 即可。

qwq
#include<bits/stdc++.h>
#define int long long 
using namespace std;

const int N = 800 + 10;

int n, f[N][N], g[N], a[N];

int Get(int id, int val){
	int opt = (id - val) / 2;
	if(opt > id / 2 || ((id - val) & 1) || val > id) return -1;
	return opt;
}

void solve(){
	cin >> n; memset(f, 0x3f, sizeof f); memset(g, 0, sizeof g); int INF = f[0][0];
	for(int i = 1; i <= n; i++) cin >> a[i], f[i][i - 1] = 0;
	for(int len = 2; len <= n; len += 2){
		for(int l = 1; l + len - 1 <= n; l++){
			int r = l + len - 1;
			if(Get(l, a[l]) != -1 && Get(l, a[l]) >= f[l + 1][r - 1]) f[l][r] = Get(l, a[l]);
			for(int k = l; k < r; k++){
				f[l][r] = min(f[l][r], max(f[l][k], f[k + 1][r] - (k - l + 1) / 2));
			}
//			if(f[l][r] > (l - 1) / 2 || (r - l + 1) % 2) f[l][r] = INF;
//			cout << l << " " << r << " " << f[l][r] << "\n";
		}
	}
	for(int i = 2; i <= n; i++){
		g[i] = g[i - 1];
		for(int j = 1; j <= i; j++) if(g[j - 1] >= f[j][i]) g[i] = max(g[i], g[j - 1] + (i - j + 1) / 2);
//		cout << g[i] << "\n";
	}
	cout << g[n] << "\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	int T; cin >> T; while(T--) solve();
	
	return 0;
}

ABC176F Brave CHAIN

容易有简单 \(O(n^3)\) \(\mathrm {DP}\):设 \(f_{i, a, b}\) 是前 \(3\times i + 2\) 个保留 \((a, b)\) 时的最大答案。有一下三类转移:

  • 不换卡:\(f_{i, a, b} = f_{i - 1, a, b} + [c = d = e]\)

  • 换一张:\(f_{i, a, c} = f_{i - 1, a, b} + [b = d = e]\)

  • 换两张:\(f_{i, c, d} = f_{i - 1, a, b} + [a = b = e]\)

当然有一些类似的不予考虑。我们发现其实没有多少状态被影响到了,若被影响到的多,原因也是 \(c = d = e\),于是猜测影响到的状态不多。接下来考虑什么样的状态 \(f_{i, a, b}\) 可能会被这三类转移分别更新。

对于第一种,显然对于任意 \(f_{i, a, b}\) 有没有被更新取决于 \([c = d = e]\),而这三者又是固定的,于是维护一个全局加标记即可。

对于第二种,被影响到的状态只有 \(c\) 这一列,若 \([b = d = e]\) 成立,此时转移过来的状态是固定的。若不满足,其实就是对于固定的 \(a\),让 \(f_{i, a, c}\) 更新为这一行的最大值,维护一下即可。

对于第三种,若满足 \([a = b = e]\),两边都是确定的。直接转移即可。若不满足,显然等价于更新为全局最大值,也是维护一下即可。

qwq
#include<bits/stdc++.h>
using namespace std;

const int N = 2000 + 10;

int n, f[N][N], arr[N * 3];
void MAX(int& x, int y){if(x < y) x = y;}

struct opt{
	int k0, k1, val;
};
void upd(int a, int b, int w){
	MAX(f[a][b], w); MAX(f[a][0], w); MAX(f[0][0], w);
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n; memset(f, -0x3f, sizeof f); int allsum = 0;
	for(int i = 1; i <= 3 * n; i++) cin >> arr[i];
	upd(arr[1], arr[2], 0); upd(arr[2], arr[1], 0);
	for(int i = 1; i < n; i++){
		queue<opt> Q; int c = arr[3 * i], d = arr[3 * i + 1], e = arr[3 * i + 2], ad = 0;
		// f[i][a][b]
		if(c == d && d == e) ad++, allsum++;
		// f[i][a][c], f[i][a][d], f[i][a][e]
		for(int a = 1; a <= n; a++){
			if(d == e) Q.push((opt){a, c, f[a][d] + 1});
			Q.push((opt){a, c, f[a][0]});
			if(c == e) Q.push((opt){a, d, f[a][c] + 1});
			Q.push((opt){a, d, f[a][0]});
			if(d == c) Q.push((opt){a, e, f[a][d] + 1});
			Q.push((opt){a, e, f[a][0]});
		}
		// f[i][c][d], f[i][c][e], f[i][d][e]
		Q.push((opt){c, d, f[e][e] + 1}); Q.push((opt){c, d, f[0][0]});
		Q.push((opt){c, e, f[d][d] + 1}); Q.push((opt){c, e, f[0][0]});
		Q.push((opt){d, e, f[c][c] + 1}); Q.push((opt){d, e, f[0][0]});
		// update
		while(!Q.empty()){
			opt ths = Q.front(); Q.pop(); 
			int a = ths.k0, b = ths.k1, w = ths.val - ad;
			upd(a, b, w); upd(b, a, w);
		}
	}
	int ans = 0;
	for(int a = 1; a <= n; a++){
		for(int b = 1; b <= n; b++){
			MAX(ans, f[a][b] + (a == b && b == arr[n * 3]));
		}
	}
	cout << ans + allsum;

	return 0;
}
/*
f[i][a][b] = f[i - 1][a][b] + [c = d = e]
f[i][a][c] = f[i - 1][a][b] + [b = d = e]
f[i][c][d] = f[i - 1][a][b] + [a = b = e]
*/

AGC007E Shik and Travel

非常厉害的一道题!

首先容易发现答案具有单调性,于是先通过二分答案 \(V\) 转化成为判定问题。

接下来注意到一条边只能走两次,其实就是限制了当进入一棵子树,必须先把里面的所有点经过之后才能离开这棵子树。也就是说,当处理到一颗子树时,需要把它的一颗子树处理完,然后从一个叶子到另一颗子树的叶子,再处理完这个子树,然后离开。

不难发现我们只需要知道两颗子树从 \(u\) 到第一个叶子的距离以及最后一个叶子到 \(u\) 的距离。于是容易设计出状态 \(f_{u, a, b}\) 表示是否有一种处理方案满足费用 \(\le V\),且 \(u\) 到第一个叶子的距离为 \(a\),最后一个叶子到 \(u\) 的距离为 \(b\)。状态转移就是

\[f_{u, a, b} = f_{ls, a - da, i - da} \& f_{rs, j - db, b - db} \& [i + j \le V] \]

但是想要再优化就没有那么简单了。首先需要注意到一个显然的但是容易被忽略的性质:若 \(x_1 \le x_2, y_1 \le y_2\)\(f_{x_2, y_2}\)\(f_{x_1, y_1}\) 严格偏序,即没有用。于是我们可以将这些状态删掉,只记录有用的状态中值为 \(1\) 的,以减小时间复杂度。此时对于一个 \(u\),其有用的 \(f\) 值显然是满足 \(a\) 单调递增,\(b\) 单调递减。于是可以双指针合并一下两颗子树中的信息。

但是这样的时间复杂度?我们设节点 \(u\) 中储存的有用的状态数为 \(siz_u\)。一次转移产生的状态数上限是 \(2 \times \min(siz_{ls}, siz_{rs})\),不难发现这其实等价于一个启发式合并,于是时间复杂度为 \(O(n \log n)\)

code:

qwq
#include<bits/stdc++.h>
#define ll long long
#define pir pair<ll, ll>
using namespace std;

const int N = 2e5 + 10;

int n, son[N][3];
ll dis[N][2];
vector<pir> f[N], g[N], vec[N];
ll lim;

void clrvec(vector<pir> &v){vector<pir> __qwq; swap(__qwq, v);}

void Merge_array(int u){
    int j = 0; vector<pir> tmp;
    for(int i = 0; i < f[u].size(); i++){
           while(j < g[u].size() && (g[u][j].first < f[u][i].first || (g[u][j].first == f[u][i].first && g[u][j].second < f[u][i].second))) tmp.push_back(g[u][j++]);
           tmp.push_back(f[u][i]);
    }
    while(j < g[u].size()) tmp.push_back(g[u][j++]);
    if(tmp.empty()) return;
    int ttt = 0; vec[u].push_back(tmp[0]);
    for(int i = 1; i < tmp.size(); i++){
            if(tmp[i].second < vec[u][ttt].second) ttt++, vec[u].push_back(tmp[i]); 
    }
}

void dfs(int u){
    if(!son[u][2]){ vec[u].push_back(make_pair(0ll, 0ll)); return;}
    for(int i = 0; i < 2; i++) dfs(son[u][i]);
    int ls = son[u][0], rs = son[u][1], j = 0, sum = dis[u][0] + dis[u][1];
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) f[u].push_back(make_pair(vec[ls][i].first + dis[u][0], vec[rs][j - 1].second + dis[u][1]));
    }
    swap(ls, rs); j = 0;
    for(int i = 0; i < vec[ls].size(); i++){
           while(j < vec[rs].size() && vec[ls][i].second + vec[rs][j].first + sum <= lim) j++;
           if(j) g[u].push_back(make_pair(vec[ls][i].first + dis[u][1], vec[rs][j - 1].second + dis[u][0]));
    }
    Merge_array(u); clrvec(f[u]); clrvec(g[u]);
    //cout << u << " " << vec[u].size() << " qwq" << "\n";
    //for(int i = 0; i < vec[u].size(); i++) cout << vec[u][i].first << " " << vec[u][i].second << "\n";
}


bool check(ll V){
       //cout << "\n" << V << "\n" << ":::" << "\n";
       for(int i = 1; i <= n; i++) clrvec(f[i]), clrvec(g[i]), clrvec(vec[i]);
       lim = V; dfs(1);
       return vec[1].size();
}

signed main(){
       ios::sync_with_stdio(0);
       cin.tie(0); cout.tie(0);
       cin >> n;
       for(int i = 2; i <= n; i++){
            int x, y; cin >> x >> y;
            son[x][son[x][2]] = i; dis[x][son[x][2]] = y; ++son[x][2];
       }
       ll l = 0, r = 3e10, ans = 3e10;
       while(l <= r){
            ll mid = (l + r >> 1);
            if(check(mid)) r = mid - 1, ans = mid;
            else l = mid + 1;
       }
       cout << ans;

       return 0;
}

P6383 『MdOI R2』Resurrection

首先观察答案形态,不难发现等价于每次找到当前树上两点所在连通块的根进行连边。现在考虑断边 \((u, v)\) 形成的 \(G\) 的形态,可以发现这个东西只跟 \(1 \to v\) 上每条边断边时间的相对关系有关。现在考虑这些边断边的相对时间 \(t_0, t_1, ..., t_m\) 以及点 \(u_1, u_2, ...., u_m\),观察出 \(u_m = u\) 会与 \(u_p(p = \max\{p|t_p < t_m\})\) 连边。那么当 \(t_0, ..., t_{m - 1}\) 确定时,不同的 \(t_m\) 可以造成不同的图 \(G\) 的个数为 \(\sum_{i = 0}^{m - 1}[{t_i > \max_{j = 0}^{i - 1}\{t_j\}]}\)。其实就是 \(t_0, ..., t_m\) 单调递增的单调栈大小,那么当 \(u\) 的单调栈大小为 \(w_u\) 的时候,\(v\) 的单调栈大小就可以是 \([2, w_u + 1]\)

所以不难设计出 \(\mathrm {DP}\) 状态:\(f_{u, i}\) 表示 \(1 \to u\) 的单调栈大小为 \(i\) 时,\(u\) 子树内不同方案数。根据上面的分析,有转移 \(f_{u, i} = \prod_{v \in son(u)}\sum_{j = 2}^{i + 1}{f_{v, j}}\)。前缀和优化即可做到 \(O(n^2)\)

qwq
#include<bits/stdc++.h>
#define ll long long
#define pb emplace_back
using namespace std;

const int N = 3e3 + 10, mod = 998244353;

int n;
ll f[N][N], s[N];
vector<int> G[N];
int Mod(int x){return (x % mod + mod) % mod;}

void dfs(int u, int fa){
	for(int j = 1; j <= n; j++) f[u][j] = 1;
	if(u != n) f[u][1] = 0;
	for(auto v : G[u]){
		if(v == fa) continue; dfs(v, u); 
		for(int j = 1; j <= n; j++) s[j] = (s[j - 1] + f[v][j]) % mod; 
		for(int j = 1; j <= n; j++) f[u][j] = f[u][j] * Mod(s[j + 1] - s[1]) % mod;
	}
//	for(int i = 1; i <= n; i++) cout << u << " " << i << " " << f[u][i] << "\n";
}

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0); cout.tie(0);
	cin >> n;
	for(int i = 1; i < n; i++){
		int x, y; cin >> x >> y;
		G[x].pb(y); G[y].pb(x);
	}
	dfs(n, 0); cout << f[n][1] << "\n";

	return 0;
}
posted @ 2024-10-06 09:51  Little_corn  阅读(1)  评论(0编辑  收藏  举报