Codeforces 1695D. Tree Queries
Easy Version
\(\texttt{Difficulty:2200}\)
题目大意
一棵 \(n(1\le n\le2000)\) 个节点的树,有一个未知节点 \(x\) ,每次询问一个节点 \(v\) ,得到 \(dis(v,x)\) ,求最少询问几个节点,在所有情况下,都能够唯一确定 \(x\) 。
思路
考虑树形 \(dp\) 。设 \(f_v\) 为在 \(v\) 为根的子树中,当 \(v\) 的向上子树已经有节点被询问的情况下,确定任意一点所需的最少查询数,记 \(cnt_v\) 为 \(v\) 的儿子中 \(f=0\) 的数量,我们当前仅需要能够确定节点在哪个子树中即可,因为我们进入任意的子树后都可以去确定节点。当 \(cnt>2\) 时,显然无法确定,否则一定可以确定,因为如果节点 \(x\) 在没有询问的子树当中,对任意其他子树中被询问的点为 \(y\) ,记 \(v\) 向上子树中被询问的一个点为 \(z\) ,于是一定有 \(dis(z,x)+dis(z,y)-2\cdot dis(z,v)\) ,否则存在不满足(\(=\) 变为 \(>\))的点。那么我们如果 \(cnt_v\) 过大,我们对没有询问的子树补上一个询问节点即可,于是可以得出转移方程:
此外叶节点的 \(f=0\),最后答案为 \(f_root+1\) 通过强制选取跟来保证各个节点满足状态定义,由于根对答案有影响,要枚举根取最小值,复杂度 \(O(n^2)\) 。
代码
#include<bits/stdc++.h>
#include<unordered_map>
#include<unordered_set>
using namespace std;
using LL = long long;
using LD = long double;
using ULL = unsigned long long;
using PII = pair<int, int>;
using TP = tuple<int, int, int>;
#define all(x) x.begin(),x.end()
#define mst(x,v) memset(x,v,sizeof(x))
#define mk make_pair
//#define int LL
//#define lc P*2
//#define rc P*2+1
#define endl '\n'
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#pragma warning(disable : 4996)
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
const double eps = 1e-8;
const LL MOD = 1000000009;
const LL mod = 998244353;
const int maxn = 200010;
int T, N, f[maxn];
vector<int>G[maxn];
void add_edge(int from, int to)
{
G[from].push_back(to);
G[to].push_back(from);
}
void dfs(int v, int p)
{
int cnt = 0;
for (auto& to : G[v])
{
if (to == p)
continue;
dfs(to, v);
if (!f[to])
cnt++;
f[v] += f[to];
}
f[v] += max(cnt - 1, 0);
}
void solve()
{
if (N == 1)
{
cout << 0 << endl;
return;
}
int ans = N;
for (int i = 1; i <= N; i++)
{
dfs(i, 0), ans = min(ans, f[i] + 1);
for (int j = 1; j <= N; j++)
f[j] = 0;
}
cout << ans << endl;
}
int main()
{
IOS;
cin >> T;
while (T--)
{
cin >> N;
for (int i = 1; i <= N; i++)
G[i].clear();
int x, y;
for (int i = 1; i < N; i++)
cin >> x >> y, add_edge(x, y);
solve();
}
return 0;
}
Hard Version
\(\texttt{Difficulty:2300}\)
与 \(\texttt{Easy Version}\) 唯一的区别是 \(1\le n\le2\cdot10^5\) 。
思路
在简单版本的做法中,我们为了保证状态的定义,强制选取了根,但我们也可以不选取根,如果一个节点的度 \(>2\) ,那么以该节点为根时,其至少两个儿子的子树内有节点被选,于是可以保证整棵树的任意一个节点的向上子树中都有节点被选,于是该根可以不用被选取,于是我们找一个这样的跟,进行一遍简单版本的 \(dp\) 即为答案,注意此时最后的答案不需要 \(+1\) ,此外如果没有这样的根,说明为一条链,答案为 \(1\) 。
代码
#include<bits/stdc++.h>
#include<unordered_map>
#include<unordered_set>
using namespace std;
using LL = long long;
using LD = long double;
using ULL = unsigned long long;
using PII = pair<int, int>;
using TP = tuple<int, int, int>;
#define all(x) x.begin(),x.end()
#define mst(x,v) memset(x,v,sizeof(x))
#define mk make_pair
//#define int LL
//#define lc P*2
//#define rc P*2+1
#define endl '\n'
#define inf 0x3f3f3f3f
#define INF 0x3f3f3f3f3f3f3f3f
#pragma warning(disable : 4996)
#define IOS ios::sync_with_stdio(0),cin.tie(0),cout.tie(0)
const double eps = 1e-8;
const LL MOD = 1000000009;
const LL mod = 998244353;
const int maxn = 200010;
int T, N, f[maxn], deg[maxn];
vector<int>G[maxn];
void add_edge(int from, int to)
{
G[from].push_back(to);
G[to].push_back(from);
}
void dfs(int v, int p)
{
int cnt = 0;
for (auto& to : G[v])
{
if (to == p)
continue;
dfs(to, v);
if (!f[to])
cnt++;
f[v] += f[to];
}
f[v] += max(cnt - 1, 0);
}
void solve()
{
if (N == 1)
{
cout << 0 << endl;
return;
}
int root = 0;
for (int i = 1; i <= N; i++)
{
if (deg[i] > 2)
{
root = i;
break;
}
}
if (!root)
{
cout << 1 << endl;
return;
}
dfs(root, 0);
cout << f[root] << endl;
}
int main()
{
IOS;
cin >> T;
while (T--)
{
cin >> N;
for (int i = 1; i <= N; i++)
G[i].clear(), deg[i] = 0, f[i] = 0;
int x, y;
for (int i = 1; i < N; i++)
cin >> x >> y, add_edge(x, y), deg[x]++, deg[y]++;
solve();
}
return 0;
}