合约数 (树形前缀和)

原题埃森哲杯第十六届上海大学程序设计联赛春季赛暨上海高校金马五校赛 B

题意

给出n个点的价值,根为q,已经n-1条边,求下面这个数据的和%(1e9+7)

以i为根的子树中,val[i]的合约数的个数*i

合约数:合数且是约数

解析

不能每个点分开算一遍,所以要想一个办法在一次遍历中解决所有问题

题目求的是一个结点为根的子树中所有合约数的个数,那么我们可以仿照前缀和的做法

首先预处理素数筛和每个数的合约数,放在vector<int>yin里面,从root开始往下遍历,用map记录下所有数出现的次数。

假设p结点的价值是8,遍历到p之前已经有2个4,3个8,而等把p结点的子树遍历完后回溯到p的时候有4个4,4个8,那么就是说这棵子树包括了2个4,1个8

另外,质数不可能的合约数,也不可能有合约数,所以可以直接跳过

代码

#include<iostream>
#include<cstdio>
#include<cmath>
#include<string>
#include<cstring>
#include<algorithm>
#include<set>
#include<map>
#include<list>
#include<vector>
#include<stack>
#include<queue>
#include<ctime>
#include<cstdlib>
#include<sstream>
#include<functional>
#define D long long
#define F double
#define MAX 0x7fffffff
#define MIN -0x7fffffff
#define mmm(a,b) memset(a,b,sizeof(a))
#define pb push_back
#define mk make_pair
#define fi first
#define se second
#define pill pair<int, int>
#define for1(i,a,b) for(int i=a;i<=b;i++)
#define for2(i,a,b) for(int i=a;i>=b;i--)
#define ini(n) scanf("%d",&n)
#define inll(n) scanf("%lld",&n)
#define outisp(n) printf("%d ",n)
#define outllsp(n) printf("%lld ",n)
#define outiel(n) printf("%d\n",n)
#define outllel(n) printf("%lld\n",n)
using namespace std;
#define N 20100
#define mod ((int)1e7+7)
#define random(a,b) (rand()%(b-a+1)+a)


int ispri[N],pri[N],now;
vector<int>yin[N];
void init(){
    ispri[1]=1;
    for(int i=2;i<=N/2;i++)ispri[i]=1;
    for(int i=2;i<=N/2;i++){
        if(ispri[i])pri[++now]=i;
        for(int j=1;j<=now&&pri[j]*i<=N/2;j++){
            ispri[pri[j]*i]=0;
        }
    }
    for(int i=4;i<=N/2;i++){
        if(ispri[i])continue;
        for(int j=4;j<=i;j++){
            if(i%j==0&&!ispri[j])yin[i].push_back(j);
        }
    }
}
int n,ro;
vector<int>v[N];
int val[N],ans[N];
map<int,int>ti;
void dfs(int p,int f){
    int vv=val[p];
    if(!ispri[vv]){
        int sum=0;
        for(int i=0;i<yin[vv].size();i++){
            int tmp=yin[vv][i];
            sum+=ti[tmp];
        }
        ti[vv]++;
        for(int i=0;i<v[p].size();i++){
            if(v[p][i]==f)continue;
            dfs(v[p][i],p);
        }
        int sum1=0;
        for(int i=0;i<yin[vv].size();i++){
            int tmp=yin[vv][i];
            sum1+=ti[tmp];
        }
        ans[p]=sum1-sum;
    }
    else{
        ans[p]=0;
        for(int i=0;i<v[p].size();i++){
            if(v[p][i]==f)continue;
            dfs(v[p][i],p);
        }
    }
}
int main(){
    init();
    int t;scanf("%d",&t);while(t--){
        scanf("%d%d",&n,&ro);
        for(int i=1;i<=n;i++)v[i].clear();
        ti.clear();
        for(int i=1;i<n;i++){
            int a,b;scanf("%d%d",&a,&b);
            v[a].push_back(b);v[b].push_back(a);
        }
        for(int i=1;i<=n;i++){
            scanf("%d",val+i);
        }
        dfs(ro,-1);
        D anss=0;
        for(int i=1;i<=n;i++){
            anss=(anss+(D)ans[i]*(D)i)%mod;
        }
        printf("%lld\n",anss);
    }
}
























### 前缀和算法在树结构中的应用与实现 #### 1. 树结构中前缀和的概念 前缀和是一种用于优化计算的技术,在树形结构中可以用来加速某些特定类型的查询操作。例如,给定一颗树以及一些路径上的权重值,可以通过维护这些路径的前缀和来快速完成诸如“某条路径上所有节点权值之和”的查询[^2]。 #### 2. 实现方式 为了实现在树结构中的前缀和运算,通常会采用哈希表存储从根节点到当前节点路径上的累积和(即所谓的前缀和)。下面是一个基于DFS遍历的方法: ```python from collections import defaultdict def tree_prefix_sum(root, target_sum): prefix_sums = {0: 1} # 初始化前缀和字典,默认有一个前缀和为0的情况 result_count = 0 def dfs(node, current_sum): nonlocal result_count if not node: return # 更新当前路径总和 current_sum += node.val # 计算满足条件的目标差值,并查找是否有对应的前缀和 difference = current_sum - target_sum result_count += prefix_sums.get(difference, 0) # 将当前前缀和加入字典 prefix_sums[current_sum] = prefix_sums.get(current_sum, 0) + 1 # 遍历左右子树 if node.left: dfs(node.left, current_sum) if node.right: dfs(node.right, current_sum) # 回溯时移除该前缀和的影响 prefix_sums[current_sum] -= 1 dfs(root, 0) return result_count ``` 此代码片段展示了如何通过深度优先搜索(DFS)的方式计算树中具有指定目标和的所有路径数量。 #### 3. 应用实例 假设有一颗二叉树,其中每个节点都有一个整数值作为它的键值。我们需要找到有多少种不同的路径使得这条路径上的节点值加起来等于预先设定的一个数`target_sum`。这里定义一条有效路径是从任意起点出发到达另一个节点结束的过程,不一定要经过根节点也不一定延伸至叶子节点[^2]。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值