首页 > 其他分享 >树上组合计数

树上组合计数

时间:2023-05-04 11:33:56浏览次数:40  
标签:include 组合 int siz long 计数 kmax 树上 Mod

主要来讲一讲树上的一些有关排列组合计数的问题。

树上拓扑序

给定一棵包含 \(n\) 个节点,以 \(1\) 为根的树。求树上拓扑序个数,即求有多少种排列方式,满足每个节点的父亲排在他前面。

\(1 \le n \le 10^6\)

显然,如果没有任何限制,整棵树的方案数为 \(n!\)。

对于一棵以 \(x\) 为节点的子树,假设它有 \(size_x\) 个节点,那么这 \(size_x\) 个节点中,\(x\) 需要排在最前面,所以只有 \(\dfrac{1}{size_x}\) 种选择。每颗子树的答案相互独立,因此总答案为 \(\dfrac{n!}{\prod_{i=1}^nsize_i}\)。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e6 + 3;
const int Mod = 1e9 + 7;

struct E {
  int p, y;
} e[kmax << 1];

int n;
int h[kmax], ec;
int siz[kmax];
long long f[kmax];
long long fac[kmax], inv[kmax];
long long res = 1;

long long Pow(long long x, long long y) {
  long long r = 1;
  for (; y; y >>= 1) {
    if (y & 1) r = r * x % Mod;
    x = x * x % Mod;
  }
  return r;
}

void Addedge(int x, int y) {
  e[++ec] = {h[x], y};
  h[x] = ec;
}

void Dfs(int x) {
  siz[x] = 1;
  for (int i = h[x]; i; i = e[i].p) {
    int y = e[i].y;
    Dfs(y);
    siz[x] += siz[y]; // 求子树大小
  }
}

int main() {
  scanf("%d", &n);
  for (int i = 1, x, y; i < n; i++) {
    scanf("%d%d", &x, &y);
    Addedge(y, x); // 连向父亲节点
  }
  for (int i = 1; i <= n; i++) {
    if (siz[i]) continue; 
    Dfs(i);
  }
  res = fac[n];
  for (int i = 1; i <= n; i++) {
    res = res * Pow(siz[i], Mod - 2) % Mod; // 记录答案
  }
  printf("%lld\n", res);
  return 0;
}

F - Distributing Integers

在上一题的基础上加入换根dp

考虑当前的根为 \(x\),需要将当前的根转移到 \(son\) 上,记 \(x\) 的答案为 \(res_x\)。

那么原来的 \(siz_x\) 由 \(n\) 变成了 \(n-siz_{son}\),原来的 \(siz_{son}\) 变成了 \(n\)。

所以 \(res_{son} = res_x \times siz_{son} \div n \times n \div (n-siz_{son})=res_s\times siz_{son} \div (n-siz_{son})\)。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e6 + 3;
const int Mod = 1e9 + 7;

struct E {
  int p, y;
} e[kmax << 1];

int n;
int h[kmax], ec;
int siz[kmax];
long long f[kmax];
long long fac[kmax], inv[kmax];
long long res = 1, ans[kmax];

long long Pow(long long x, long long y) {
  long long r = 1;
  for (; y; y >>= 1) {
    if (y & 1) r = r * x % Mod;
    x = x * x % Mod;
  }
  return r;
}

void Addedge(int x, int y) {
  e[++ec] = {h[x], y};
  h[x] = ec;
}

void Dfs(int x, int fa) {
  siz[x] = 1;
  for (int i = h[x]; i; i = e[i].p) {
    int y = e[i].y;
    if (y == fa) continue;
    Dfs(y, x);
    siz[x] += siz[y];
  }
}

void Dfss(int x, int fa) {
  for (int i = h[x]; i; i = e[i].p) {
    int y = e[i].y;
    if (y == fa) continue;
    long long ress = ans[x];
    ress = ress * Pow(n - siz[y], Mod - 2) % Mod * siz[y] % Mod; // 换根
    ans[y] = ress;
    Dfss(y, x);
  }
}

int main() {
  scanf("%d", &n);
  for (int i = 1, x, y; i < n; i++) {
    scanf("%d%d", &x, &y);
    Addedge(x, y);
    Addedge(y, x);
  }
  Dfs(1, 0);
  res = fac[n];
  for (int i = 1; i <= n; i++) {
    res = res * Pow(siz[i], Mod - 2) % Mod;
  }
  ans[1] = res; // 求出1的答案
  Dfss(1, 0); // 遍历整棵树,求出其他答案
  for (int i = 1; i <= n; i++) {
    printf("%lld\n", ans[i]);
  }
  return 0;
}

树上染色

shy 有一颗 \(n\) 个节点的树,现在要用 \(k\) 种不同颜色的染料给树染色。

一个染色方案是合法的,当且仅当对于所有相同颜色的点对 \((x,y)\),\(x\) 到 \(y\) 的路径上的所有点的颜色都要与 \(x\) 和 \(y\) 相同。即对于每种颜色,要么没染,要么染成该颜色的节点只形成一个连通块。

请统计染色方案数,对 \(10^9+7\) 取模。

\(1 \le k \le n \le 10^5\)

其实与树都没有关系。

我们先枚举颜色数量,假设我们使用 \(s\) 种颜色染,那么我们需要从 \(n-1\) 条边中选出 \(s-1\) 条断开,这是 \(C(n-1,s-1)\) 的,然后我们要从 \(k\) 种颜色中选出 \(s\) 种并分配给 \(s\) 个连通块,是 \(A(k,s)\) 的。

总答案为 \(\sum_{i=1}^kC(n-1,i-1)\times A(k,i)\),要记得取模。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

const int kmax = 1e5 + 3;
const int Mod = 1e9 + 7;

struct E {
  int p, y;
} e[kmax << 1];

int n, k;
int h[kmax], ec;
long long fac[kmax], inv[kmax];
long long res;

void Addedge(int x, int y) {
  e[++ec] = {h[x], y};
  h[x] = ec;
}

long long Pow(long long x, long long y) {
  long long r = 1;
  for (; y; y >>= 1) {
    if (y & 1) r = r * x % Mod;
    x = x * x % Mod;
  }
  return r;
}

void Init() {
  fac[0] = inv[0] = 1;
  // 求阶乘
  for (int i = 1; i < kmax; i++) {
    fac[i] = fac[i - 1] * i % Mod;
  }
  // 求逆元
  inv[kmax - 1] = Pow(fac[kmax - 1], Mod - 2);
  for (int i = kmax - 2; i; i--) {
    inv[i] = inv[i + 1] * (i + 1) % Mod;
  }
}

long long C(long long x, long long y) { //组合
  return fac[x] * inv[y] % Mod * inv[x - y] % Mod;
}

long long A(long long x, long long y) { //排列
  return fac[x] * inv[x - y] % Mod;
}

int main() {
  Init();
  cin >> n >> k;
  for (int i = 1, x, y; i < n; i++) {
    cin >> x >> y;
    Addedge(x, y);
    Addedge(y, x);
  }
  for (int i = 1; i <= k; i++) {
    res = (res + C(n - 1, i - 1) * A(k, i) % Mod) % Mod; // 计算,取模
  }
  printf("%lld\n", res);
  return 0;
}

树上连通块

给定一棵包含 \(n\) 个节点的树,求出每个节点所在的连通块数量,对 \(10^9+7\) 取模。

\(1\le n \le 10^6\)

先来考虑以 \(1\) 为整棵树的根,定义 \(f_i\) 表示在以 \(i\) 为根的子树内,\(i\) 必选的连通块个数。

那么易得 \(f_i = \prod_{j\in Son_i}f_j+1\),即选该子树和不选该子树。

其他节点的答案,我们可以用换根实现。

当从点 \(x\) 转移到 \(son\) 时, \(son\) 的答案需要加上 \(x\) 这一棵子树的答案,即 \(x\) 的其他子树。

用逆元是可以被卡的,所以我们要换一种思路。

考虑计算一棵子树的时候,记录好子树前缀和后缀的答案,除去一棵子树时取前缀与后缀即可。

代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>
#include <vector>

using namespace std;

const int kmax = 1e6 + 3;
const int Mod = 1e9 + 7;

int n;
long long f[kmax], res[kmax];
vector<int> e[kmax];

void Dfs(int x, int fa) {
  f[x] = 1;
  for (int y : e[x]) {
    if (y == fa) continue;
    Dfs(y, x);
    f[x] = f[x] * (f[y] + 1) % Mod;
  }
}

void Dfss(int x, int fa, long long v) {
  res[x] = f[x] * (v + 1) % Mod;
  long long p[e[x].size() + 2], s[e[x].size() + 2];
  p[0] = s[e[x].size() + 1] = 1;
  for (int i = 1; i <= e[x].size(); i++) { // 计算前缀
    int y = e[x][i - 1];
    if (y == fa) {
      p[i] = p[i - 1];
    } else {
      p[i] = p[i - 1] * (f[y] + 1) % Mod;
    }
  }
  for (int i = e[x].size(); i; i--) { // 计算后缀
    int y = e[x][i - 1];
    if (y == fa) {
      s[i] = s[i + 1];
    } else {
      s[i] = s[i + 1] * (f[y] + 1) % Mod;
    }
  }
  for (int i = 1; i <= e[x].size(); i++) {
    int y = e[x][i - 1];
    if (y == fa) continue;
    Dfss(y, x, (v + 1) * p[i - 1] % Mod * s[i + 1] % Mod); // 取前缀和后缀
  }
}

int main() {
  cin >> n;
  for (int i = 1, x, y; i < n; i++) {
    cin >> x >> y;
    e[x].push_back(y);
    e[y].push_back(x);
  }
  Dfs(1, 0); // 先算出1的答案
  Dfss(1, 0, 0); // 换根计算
  for (int i = 1; i <= n; i++) {
    cout << res[i] << '\n';
  }
  return 0;
}

完结撒花 \ / \ / \ /

标签:include,组合,int,siz,long,计数,kmax,树上,Mod
From: https://www.cnblogs.com/ereoth/p/17370609.html

相关文章

  • 2023-05-03:给你一棵 二叉树 的根节点 root ,树中有 n 个节点 每个节点都可以被分配一个
    2023-05-03:给你一棵二叉树的根节点root,树中有n个节点每个节点都可以被分配一个从1到n且互不相同的值另给你一个长度为m的数组queries你必须在树上执行m个独立的查询,其中第i个查询你需要执行以下操作:从树中移除以queries[i]的值作为根节点的子树题目所用测试......
  • 2023-05-03:给你一棵 二叉树 的根节点 root ,树中有 n 个节点 每个节点都可以被分配一个
    2023-05-03:给你一棵二叉树的根节点root,树中有n个节点每个节点都可以被分配一个从1到n且互不相同的值另给你一个长度为m的数组queries你必须在树上执行m个独立的查询,其中第i个查询你需要执行以下操作:从树中移除以queries[i]的值作为根节点的子树题目所......
  • 四、程序计数器(PC寄存器)(基础篇)
    一、PCRegister介绍1、介绍JVM中的程序计数寄存器(ProgramCounterRegister)中,Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩......
  • 经典数学组合题——西尔维斯特问题
    题目:在一个平面内有n(n>=3)个不完全共线的点,求证:则该平面内至少存在一条线恰好穿过其中两点证明:考查这个平面上每个至少经过两点的边以及对于一条边,不在该边上的点到边的最短长度。考虑上面最短长度中最短的一条边和一个点则该边恰好经过两个点证明如下......
  • 经典数学组合题(抽屉原理)
    题目:任意mn+1个不同的数排成一列,求证:要么存在m+1项递增数列,要么存在n+1项递减数列一、分析为什么要任意mn+1个数呢?是不是说明mn个数存在不满足的情况?我们可以先尝试寻找mn个数的情况我们发现:n,n-1,...,1,  2n,2n-1,...,n-1,  ......,  mn,mn-1,...,(m-1......
  • PMP-06-项目组合管理和醒目集管理
    一、项目组合是为了实现战略目标而组合在一起管理的项目,项目集、子项目组合和运营工作。二、项目集是一组相互关联且被协调管理的项目子项目集和项目集活动。三、项目组合管理的工作可以分为几个部分,包括业务战略、活动、项目组合、战略平衡、批准授权,项目监控。四、项目集管......
  • 括号匹配序列计数问题
    题意一个长度为\(n\)且符合规范的括号序列,其有些位置已经确定了,有些位置尚未确定,求这样的括号序列一共有多少个。要求复杂度为\(O(n^3)\)分析由题意可知,我们可以这样定义括号匹配序列:空序列是括号匹配序列。如果S是括号匹配序列,那么\((S)\)也是括号匹配序列。如果......
  • CF 1709E XOR Tree(树上启发式合并)
    题目链接:https://codeforces.com/contest/1709/problem/E解题思路:定义sum(x,y)为x→y路径上的点的异或和,dx 为x→root路径上的点的异或和。对于一个点权树,sum(x,y)=dx ^dy ^vallca(x,y)。考虑修改一个点,可以将它改为一个很大的2为底数的幂,则经过此点的所有的不合......
  • 树上启发式合并学习笔记
    最近几天了解到一个很神奇的算法——dsuontree,看上去没多快实际上很快,这叫低调。好久不更了,至于反演,5月再更吧,4月的最后一天分享一下dsuontree。顺便闲话一句,4/26是我生日,也是历史二模。重链剖分dsuontree这类dsuontree适用于多次询问,每次询问需要$O(子树大小)......
  • 十三:组合模式:优雅的组织结构
    a.组合模式解读组合模式是一种结构型设计模式,它允许将对象组织成树形结构,以表示“部分-整体”的层次关系。这种模式让客户端可以统一对待单个对象和组合对象,简化了代码的复杂度。在现实生活中,我们经常会遇到这种需求,如文件系统、组织结构等。b.亲身实践:组合模式实现为了更好地......