前言

模拟赛赛时SubtaskR3没开long long丢了20分。

题意简述

题目描述

对于一棵有根树(设其节点数为 \(n\) ,则节点编号从 \(1\) 至 \(n\) ),如果它满足所有非根节点的编号均比起父亲更大,我们就说它是Y树。

此外,出题人给出了 \(k\) 个整数 \(a_1, \dots, a_k\),并规定,只要一棵有根树存在一个子树包含的节点数恰好为 \(a_1, \dots, a_k\) 中的某一个值,那么它不符合条件;

现给定 \(n,k,a1,\dots,ak\),并额外给定整数 \(L,R\) ,请你对于 \(d = L,L+1,\dots,R\) ,分别求出 \(n\) 个节点的深度为 \(d\) 的符合条件的Y树的数量。

数据范围及提示

\[\begin{matrix}
\text{测试点编号} & | & n\leq & | & k & | & \text{特殊限制} \\
------- & - & -- & - & -- & - &-----\\
1,2,3,4,5,6 & | & 10 & | & <n & | & \text{无} \\
------- & - & -- & - & -- & - &-----\\
7,8,9,10,11 & | & & | & & | & R=3 \\
------- & - & & - & & - &-----\\
12,13,14,15,16 & | & 100 & | &=0 & | & L = n - 2 \\
------- & - & & - & & - &-----\\
17,18,19 & | & & | & & | & & \\
------- & - & & - & -- & - &-----\\
20,21,22 & | & & | & <n & | & \text{无} & \\
------- & - & -- & - & & - & \\
23,24,25 & | & 500 & | & & | & \\
\end{matrix}
\]

对于所有测试点,保证 \(0 \leq k < n \leq 500\)

题解

部分分

首先讲暴力,DFS即可。

首先写一下 \(R \leq 3\) 的部分分。

很多同学赛时试图找规律,但是实际上并没有什么规律,

很显然,当树的高度为2的时候,只有一种情况,就是我们常说的"菊花图"。

那么当树的高度增加到3的时候,显然可以想到一种可行的变换,

我们保留一部分的节点,。

对于 \(L \geq n - 2\) 的部分分。

首先打表找规律,当树高 \(n - 1\) 时,方案数为 \(\frac{n \times (n - 1)}{2} - 2\)。

可以想象为一条链上拆下来一个节点,连接到另一个节点上。

减去的两种方案分别为节点 \(1\) 多计算了一次,节点 \(n\) 不能重新连接到节点 \(n-1\) 上

当树高 \(n - 2\) 时,显然从一条链上拆下来两个点,

我们可以分成两种情况来看

  1. 两个点连在一起
  2. 两个点分开

最后在减去一些零零散散的重复计算部分,就完成了。

代码

警告:以下代码为暴力代码,非常的长,可以跳过

#pragma GCC optimize(3)
#include <cstdio>
#include <cstring>
#include <vector>
#define MOD 998244353 using namespace std; int n, k, L, R;
int a[26]; namespace SubtaskBrute{
vector<int> son[26];
int ban[26], sum[26];
int d[26];
long long ans = 0;
int qry;
bool solve(int u){
int u_s = son[u].size(); sum[u] = 1;
for (int i = 0; i < u_s; ++i){
if (!solve(son[u][i])) return 0;
sum[u] += sum[son[u][i]];
}
return (!ban[sum[u]]);
}
void DFS(int u){
if (u == n + 1){ ans += solve(1); return ; }
for (int i = 1; i < u; ++i){
if (d[i] + 1 > qry) continue;
d[u] = d[i] + 1;
son[i].push_back(u);
DFS(u + 1);
son[i].pop_back();
}
}
int res[26];
void index(){
for (int i = 1; i <= k; ++i) ban[a[i]] = 1;
d[1] = 1;
for (qry = L - 1; qry <= R; ++qry){
if (!qry) res[qry] = 0;
else{ ans = 0; DFS(2); res[qry] = ans; }
}
for (int i = L; i <= R; ++i) printf("%lld ", (res[i] - res[i - 1]) % 998244353);
}
} namespace Subtask1{
int index(int num){
if (num <= 0) return 0;
puts("HJC AK IOI!");
}
} namespace SubtaskR3{
long long f[505][505];
void index(){
f[1][1] = 1;
for (int i = 2; i <= n; ++i)
for (int j = 1; j < i; ++j)
f[i][j] = (f[i - 1][j - 1] + f[i - 1][j] * j) % MOD;
long long ans = -1;
for (int j = 1; j < n; ++j)
ans = (ans + f[n][j]) % MOD;
if (L <= 1) printf("0 ");
if (L <= 2) printf("1 ");
printf("%lld", ans);
}
} namespace SubtaskL2{
inline long long getL1(long long n){
return (((n * (n - 1) >> 1) - 2) % 998244353);
}
void index(){
long long ans = 1 - (n << 1);
for (int i = 2; i < n; ++i)
for (int j = i + 1; j <= n; ++j){
ans += i - 1;
if (i == n - 1) --ans;
ans %= MOD;
}
for (int i = 2; i < n; ++i)
for (int j = i + 1; j <= n; ++j){
if (i == n - 1 && j == n)
ans = (ans + (n - 3) * (n - 3)) % MOD;
else{
ans += (i - 1) * (j - 2) % MOD;
ans %= MOD;
if (j == n - 1) ans -= (i - 1);
else if (j == n) ans -= (i - 1);
}
}
printf("%lld", (ans + MOD) % MOD);
if (R >= n - 1) printf(" %lld", getL1(n));
if (R >= n) printf(" 1");
}
} int main(){
freopen("subtree.in", "r", stdin);
freopen("subtree.out", "w", stdout);
scanf("%d %d", &n, &k);
for (int i = 1; i <= k; ++i) scanf("%d", &a[i]);
scanf("%d %d", &L, &R);
if (n <= 10){ SubtaskBrute::index(); return 0; }
else if (R == 3 && k == 0){ SubtaskR3::index(); return 0; }
else if (L == n - 2 && k == 0){ SubtaskL2::index(); return 0; }
return 0;
}

正解

非常简单的动态规划。

我们定义状态 \(f[i][d]\) 表示大小为 \(i\) ,高度为 \(d\) 的Y树种类数。

我们转移状态的时候考虑以"合并"的方式转移。

为了避免重复计算,我们定义次小节点一定合并到最小节点上(最小节点为树根即1,显然次小节点无论如何都得连接在上面)

避免了重复计算以后,我们枚举合并进来的子树的大小,可以列出如下的式子:

\[f[i][d] = \sum_{j=1}^{i-1} f[i - j][d] \times f[j][d-1] \times \left(\begin{matrix}i - 2 \\ j - 1 \end{matrix}\right)
\]

那么您可能会觉得奇怪,这还没有考虑限制条件呢?

其实我们只需要在DP时,把限制条件所限制的子树大小,DP值清零即可。

代码

#include <cstdio>
#define MOD 998244353 int C[505][505], f[505][505];
bool ban[505]; int main(){
int n, k; scanf("%d %d", &n, &k);
for (int i = 0; i <= n; ++i){
C[i][0] = 1;
for (int j = 1; j <= i; ++j)
C[i][j] = (C[i - 1][j - 1] + C[i - 1][j]) % MOD;
}
for (int i = 1; i <= k; ++i){
int x; scanf("%d", &x);
ban[x] = 1;
}
if (ban[1]){
int L, R; scanf("%d %d", &L, &R);
for (int i = L; i <= R; ++i)
printf("0%c", (i != R ? ' ' : '\n'));
return 0;
}
f[1][1] = 1;
for (int d = 2; d <= n; ++d){
f[1][d] = 1;
for (int i = 2; i <= n; ++i)
for (int j = 1; j < i; ++j)
f[i][d] = (f[i][d] + 1ll * f[i - j][d] * f[j][d - 1] % MOD * C[i - 2][j - 1]) % MOD;
for (int i = 1; i <= n; ++i)
if (ban[i]) f[i][d] = 0;
}
int L, R; scanf("%d %d", &L, &R);
for (int i = L; i <= R; ++i)
printf("%d%c", (f[n][i] - f[n][i - 1] + MOD) % MOD, (i != R ? ' ' : '\n'));
return 0;
}

[HG]子树问题 题解的更多相关文章

  1. [HG]走夜路 题解

    前言 整个机房就我一个人在想动态规划. 想了半天发现一堆性质,结果由于DP中出现折线挂了. 题目描述 某NOIP普及组原题加强版. \(Jim\) 非常怕黑,他有一个手电筒,设手电筒的电量上限为 \( ...

  2. [HG]腿部挂件 题解

    前言 暴力跑的比正解快. 以下暴力(循环展开+fread读入输出优化) #include<cstdio> #pragma GCC optimize(3, "Ofast" ...

  3. 洛谷P1122最大子树和题解

    题目 一道比较好想的树形\(DP\) 完全可以用树形DP的基本思路,递归,然后取最优的方法. \(Code\) #include <iostream> #include <cstri ...

  4. [HG]提高组 题解

    首先很容易想到暴力DP 设状态f[i][j]表示当前放了第i个数,最大的数为j的方案数. 然后根据转移推出实际上是在下图走路的方案数 \[ \left( \left( \begin{matrix} x ...

  5. 【BZOJ】2434: [Noi2011]阿狸的打字机

    题意 给你一些字符串.\(m\)次询问,每一次询问第\(x\)个字符串在\(y\)字符串中出现了多少次.(输入总长$ \le 10^5$, \(M \le 10^5\)) 分析 在ac自动机上,\(x ...

  6. BZOJ 2002: [Hnoi2010]Bounce 弹飞绵羊 LCT

    2002: [Hnoi2010]Bounce 弹飞绵羊 Time Limit: 1 Sec Memory Limit: 256 MB 题目连接 http://www.lydsy.com/JudgeOn ...

  7. 【洛谷P1272】道路重建

    题目大意:给定一个 N 个节点的树,求至少剪掉多少条边才能使得从树中分离出一个大小为 M 的子树. 题解:考虑树形 dp,定义 \(dp[u][i][t]\) 为以 u 为根节点与前 i 个子节点构成 ...

  8. 【loj#139】树链剖分

    #139. 树链剖分 题目描述 这是一道模板题. 给定一棵 $n$个节点的树,初始时该树的根为 111 号节点,每个节点有一个给定的权值.下面依次进行 $m$ 个操作,操作分为如下五种类型: 换根:将 ...

  9. [洛谷P3833][SHOI2012]魔法树

    题目大意:给一棵树,路径加,子树求和 题解:树剖 卡点:无 C++ Code: #include <cstdio> #include <iostream> #define ma ...

随机推荐

  1. 【模板】SPFA(不完全详解)

    一种最短路求法(个人觉得比DIJKSTRA好用) 用于有向图. 大概思路:从根节点开始,枚举每一个点,同时更新他们所联通的点的最短路径,如果路径被更新,则把这个点入队,一直重复这个操作直到队伍为空为止 ...

  2. Luogu P3511 [POI2010]MOS-Bridges

    题目 二分答案然后混合图欧拉回路,因为没有SPJ所以就没写了,怕写了有锅.

  3. 解决WordPress百度分享图标不显示问题

    最近在帮朋友维护博客时,发现他的百度分享居然不能使用了,首先很多人会认为,百度分享挂在那里就是一种摆设,又没有几个人去分享,有什么含义呢?其实挂百度分享的含义是非常重要的,网站增加一个百度分享是可以增 ...

  4. Springboot提示数据库连接问题Connection is not available

    2019-05-29 11:19:51.824 WARN 854 --- [io-8080-exec-10] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL ...

  5. python题

    1.一行代码实现1--100之和 利用sum()函数求和 sum(range(1,101) 2.如何在一个函数内部修改全局变量 利用global 修改全局变量 3.列出5个python标准库 Pyth ...

  6. springboot(十九)-线程池的使用

    我们常用ThreadPoolExecutor提供的线程池服务,springboot框架提供了@Async注解,帮助我们更方便的将业务逻辑提交到线程池中异步执行. 话不多说,编码开始: 1.创建spri ...

  7. 禁止ios10双指缩放

    document.addEventListener('gesturestart', function(event) { event.preventDefault(); });

  8. VUE实现国际化

    一.前言 趁着11月的最后一天,来写一篇关于前端国际化的实践型博客.国际化应该都不陌生,就是一个网站.应用可以实现语言的切换. 在这就不谈原理,只说说如何实现中英文的切换.做技术的总得先把 demo ...

  9. 利用python3 调用zabbix接口完成批量加聚合图形(screens)

    在上一篇博客中,我们完成的利用python3 调用zabbix接口批量增加主机,增加主机的item,增加主机的图形! 接下来我们完成批量增加主机的screen 首先我们要增加screen需要哪些参数呢 ...

  10. vim编辑器学习

    vim是一个非常强大的编辑器,看了很多文章能感受到它的强大,不过还需要不断地学习和使用来慢慢感受. 安装vim 在ubuntu 系统中使用 sudo apt-get install vim-gtk 安 ...