传送门
题目大意
给你大小分别为 和 的两棵树 和 。构造一张新图,该图中每一个点的编号为 。如果在 中 和 之间有边,那么在该图上对于任意的 , 和 之间有边。同样,如果在 中 和 之间有边,那么在该图上对于任意的 , 和 之间有边。问你这个图上长度为 的环有多少个。环的定义是从一个点出发,走 步回到起点,可以经过重复点和重复边,路径相同,起点不同的环看作是不同的。
, ,答案对 取模。
思路
什么都不会的感觉……
考虑子问题。如果直接给你一张图,让你求出走 步回到原点的方案数可以怎么做?由于可以经过重复点和重复边,因此设 表示走 步到 的方案数,边界条件为 ,最终答案为 。 个点的时间复杂度为 或者 。
现在我们要求从每个点出发的答案之和,显然我们不能写一个 的算法……
显然我们甚至不能把这张新的图建出来,因此我们考虑下这个新图到底是什么意思。我们把它看作一个二维坐标 ,每次移动要么根据 改变 ,要么根据 改变 。这样我们就明白了,原来可以把问题抽象到这两棵树上,看作两棵树上分别有一个棋子,我们每次能够选一个棋子走一步,问操作 次后两个棋子都在原位的方案数。
考虑枚举在第一棵树上走了多少步。设
表示在第一棵树上走了
步回到起点的方案数,
表示在第二棵树上走了
步回到起点的方案数,那么最终答案为:
等于从第一棵树上的每个结点出发走 步回到自己的方案数之和;注意要乘以一个组合数。如果沿用前面的思路,那么不可避免的每个结点出发都要算一遍,时间复杂度为 ,不足以通过此题。
有没有更好的做法呢?
由于不同的路径对于所有不同的起点都有贡献,那我们考虑一下,对于一条路径能不能让所有经过的点都算上贡献。考虑点分治,每次统计经过分治中心的路径对所有点的答案的贡献。注意到,对于一个点来说,一条回路必然呈花瓣状,于是我们求两个东西: 和 。 表示从分治中心出发走 步停在 点的方案数,要求中间不经过分治中心(也就是一个花瓣), 意义与 相同,但是没有限制(也就是任意多个花瓣)。对于分治中心而言,贡献就是 ,但是对于其它点 而言,我们考虑这样两条路径:从分治中心出发,不经过分治中心到达 ;从分治出发,可以经过分治中心到达 。把第二条路径反转,我们就得到一条从 出发的回路。我们枚举第一条路径的长度(这条路径必然是一个花瓣的一部分,就不会算重了),然后用 的时间复杂度做卷积即可。时间复杂度 。
参考代码
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cstring>
#include <cassert>
#include <cctype>
#include <climits>
#include <ctime>
#include <iostream>
#include <algorithm>
#include <vector>
#include <string>
#include <stack>
#include <queue>
#include <deque>
#include <map>
#include <set>
#include <bitset>
#include <list>
#include <functional>
typedef long long LL;
typedef unsigned long long ULL;
using std::cin;
using std::cout;
using std::endl;
typedef int INT_PUT;
INT_PUT readIn()
{
INT_PUT a = 0; bool positive = true;
char ch = getchar();
while (!(ch == '-' || std::isdigit(ch))) ch = getchar();
if (ch == '-') { positive = false; ch = getchar(); }
while (std::isdigit(ch)) { a = a * 10 - (ch - '0'); ch = getchar(); }
return positive ? -a : a;
}
void printOut(INT_PUT x)
{
char buffer[20]; int length = 0;
if (x < 0) putchar('-'); else x = -x;
do buffer[length++] = -(x % 10) + '0'; while (x /= 10);
do putchar(buffer[--length]); while (length);
putchar('\n');
}
const int mod = 998244353;
inline void add(int& a, const int& b)
{
register int t;
a = (t = a + b) >= mod ? t - mod : t;
}
const int maxn = 4005;
const int maxk = 80;
int k;
typedef std::vector<std::vector<int> > Graph;
struct Tree
{
int n;
Graph G;
void resize(int size)
{
n = size;
G.resize(size + 1);
}
void read()
{
for (int i = 2; i <= n; i++)
{
int from = readIn();
int to = readIn();
G[from].push_back(to);
G[to].push_back(from);
}
}
bool vis[maxn];
int size[maxn];
void DFS1(int node, int parent)
{
size[node] = 1;
for (int i = 0; i < G[node].size(); i++)
{
int to = G[node][i];
if (to == parent || vis[to]) continue;
DFS1(to, node);
size[node] += size[to];
}
}
int findRoot(int node, int parent, int s)
{
for (int i = 0; i < G[node].size(); i++)
{
int to = G[node][i];
if (to == parent || vis[to])
continue;
if (size[to] > (s >> 1))
return findRoot(to, node, s);
}
return node;
}
std::vector<std::pair<int, int> > transfer[2];
std::vector<int> nodes;
int f[maxn][maxk];
int g[maxn][maxk];
void DFS2(int node, int parent)
{
nodes.push_back(node);
std::memset(f[node], 0, sizeof(f[node]));
std::memset(g[node], 0, sizeof(g[node]));
for (int i = 0; i < G[node].size(); i++)
{
int to = G[node][i];
if (to == parent || vis[to])
continue;
transfer[0].push_back(std::make_pair(node, to));
transfer[1].push_back(std::make_pair(node, to));
transfer[1].push_back(std::make_pair(to, node));
if (node != nodes.front())
transfer[0].push_back(std::make_pair(to, node));
DFS2(to, node);
}
}
void solve(int node)
{
DFS1(node, 0);
node = findRoot(node, 0, size[node]);
vis[node] = true;
transfer[0].clear();
transfer[1].clear();
nodes.clear();
DFS2(node, 0);
f[node][0] = g[node][0] = 1;
for (int i = 1; i <= k; i++)
{
for (int j = 0; j < transfer[0].size(); j++)
{
const std::pair<int, int>& trans = transfer[0][j];
add(f[trans.second][i], f[trans.first][i - 1]);
}
for (int j = 0; j < transfer[1].size(); j++)
{
const std::pair<int, int>& trans = transfer[1][j];
add(g[trans.second][i], g[trans.first][i - 1]);
}
}
for (int i = 0; i <= k; i++)
add(ans[i], g[node][i]);
for (int i = 1; i < nodes.size(); i++)
{
int t = nodes[i];
for (int j = 0; j <= k; j++)
for (int l = 0; l <= j; l++)
ans[j] = (ans[j] + (LL)f[t][l] * g[t][j - l]) % mod;
}
for (int i = 0; i < G[node].size(); i++)
{
int to = G[node][i];
if (vis[to]) continue;
solve(to);
}
}
int ans[maxk];
void run()
{
nodes.reserve(maxn);
transfer[0].reserve(maxn * 2);
transfer[1].reserve(maxn * 2);
solve(1);
}
const int& operator[](int x) const { return ans[x]; }
} tree[2];
int C[maxk][maxk];
void init()
{
C[0][0] = 1;
for (int i = 1; i <= k; i++)
{
C[i][0] = 1;
for (int j = 1; j <= i; j++)
{
register int t;
C[i][j] = (t = C[i - 1][j - 1] + C[i - 1][j]) >= mod ? t - mod : t;
}
}
}
void run()
{
tree[0].resize(readIn());
tree[1].resize(readIn());
k = readIn();
for (int i = 0; i < 2; i++)
{
tree[i].read();
tree[i].run();
}
init();
int ans = 0;
for (int i = 0; i <= k; i++)
add(ans, (LL)C[k][i] * tree[0][i] % mod * tree[1][k - i] % mod);
printOut(ans);
}
int main()
{
run();
return 0;
}
总结
首先要知道如何用动态规划求解一般图走 步的回路的方案数,然后要把这道题转化模型,前两步做出来后就能写出一个合法的暴力了。要想出点分治的方法,应该要从“贡献一起算”出发,进而想到从某个点出发引两条路径,把其中一条路径反转,进而想到点分治。