2017 Chinese Multi-University Training 1(C(树形dp)+F(置换群循环节)+H(nth_element)+I(仙人掌第k大生成树)+L(组合数学+dfs))

心得

实质上的,第一次三人合作上机

暴露出很多不足,多练才能变强

D(NTT)E(dfs+高维前缀和)G(dfs序)J(生成函数+FFT)待补

思路来源

https://blog.csdn.net/qq_31759205/article/details/76154626 H题

https://blog.csdn.net/ME495/article/details/76165039 I题

https://www.cnblogs.com/chen9510/p/7258595.html?utm_source=itdadao&utm_medium=referral L题

https://blog.csdn.net/sunsiyou/article/details/97299155 待补题

赛后补题

HDU 6035 Colorful Tree(树形dp)

最多50组样例,每次给定n(n<=2e5)个点的一棵树,点i有一种颜色ci(1<=ci<=n)

无序点对(u,v)的贡献,定义为u到v这条路径(含u和v)上不同颜色的种数

求所有无序点对的贡献之和

 

先假设每条路径都出现了n种颜色,再减去每条路径没出现过的颜色

单独考虑一种颜色c,则c把这棵树的其余区域划分成了不同块,

每个块内都没有出现c,所以应当减去的是\sumC_{x}^{2}(x为每个块的大小)

考虑dfs的过程,开始u的贡献认为是子树v,后来v减去了所有u的子树之后的x为这块的真实sz

特别地,如果c没有在这棵树中出现,sz显然为n,这棵树的大小

有一遍dfs的写法,树形dp还是不怎么熟练

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define pb push_back
using namespace std;
typedef long long ll;
const int N=2e5+10;
vector<int>E[N];
int c[N],u,v;
int n,sz[N],num[N];
ll ans;
ll C(ll x)
{
    return 1ll*x*(x-1)/2;
}
void dfs(int u,int fa)
{
    sz[u]=1;
    for(int v:E[u])
    {
        if(v==fa)continue;
        dfs(v,u);
        sz[u]+=sz[v];
    }
}
void dfs2(int u,int fa)
{
    int tmp=num[c[u]];
    for(int v:E[u])
    {
        if(v==fa)continue;
        num[c[u]]=sz[v];
        dfs2(v,u);
        ans-=C(num[c[u]]);
    }
    num[c[u]]=tmp-sz[u];
}
int main()
{
    int ca=1;
    while(~scanf("%d",&n))
    {
        for(int i=1;i<=n;++i)
        {
            E[i].clear();
            num[i]=sz[i]=0;
        }
        for(int i=1;i<=n;++i)
        {
            scanf("%d",&c[i]);
        }
        for(int i=1;i<n;++i)
        {
            scanf("%d%d",&u,&v);
            E[u].pb(v),E[v].pb(u);
        }
        dfs(1,-1);
        for(int i=1;i<=n;++i)
        num[i]=sz[1];
        ans=C(n)*n;
        dfs2(1,-1);
        for(int i=1;i<=n;++i)
        ans-=C(num[i]);
        printf("Case #%d: %lld\n",ca++,ans);
    }
    return 0;
}

后续:atcoder163F出到了这道题,近乎一模一样,于是补了一下这道题只需要一次dfs的写法

#include<iostream>
#include<cstdio>
#include<cstring>
#include<vector>
#define pb push_back
using namespace std;
typedef long long ll;
const int N=2e5+10;
int n,c[N],sz[N],all,now[N],u,v;//all表示dfs已经访问了多少点
//now[c[u]]:表示以u为根向下看 以相同的颜色c[u]为根的点的子树的数量的和 它们是不统计在中间的块内的
vector<int>e[N];
ll ans;
void dfs(int u,int fa){
	sz[u]=1;
	int tmp=now[c[u]];//进入u的子树前now[c[u]]的值 
	for(auto &v:e[u]){
		if(v==fa)continue;
		int pre=all-now[c[u]];
		dfs(v,u);
		sz[u]+=sz[v];
		int nex=all-now[c[u]];//all+=v的这棵子树大小 now[c[u]]+=v的子树里与u相同的所有子树大小之和 all-now[c[u]]的变化量 实际是对v这棵子树dfs后 子树大小的增量-被隔离的相同子树的大小增量=中间挤出的块的大小的增量 
		ans-=1ll*(nex-pre)*(nex-pre-1)/2;//减去C(增量,2)的贡献
	}
	all++;//已dfs的点数
	now[c[u]]=tmp+sz[u];//离开u的子树后now[c[u]]的值 需要加上u的这棵子树
}
int main(){
	int ca=0;
	while(~scanf("%d",&n)){
		ans=1ll*n*(n-1)/2*n;//n种颜色 每种C(n,2)
		all=0;
		for(int i=1;i<=n;++i){
			e[i].clear();
			now[i]=0;
			scanf("%d",&c[i]);
		} 
		for(int i=1;i<n;++i){
			scanf("%d%d",&u,&v);
			e[u].pb(v);e[v].pb(u);
		}
		dfs(1,-1);
		for(int i=1;i<=n;++i){
			int pre=0,nex=all-now[i];//把虚根-1看成是各种颜色 最后清一次 连接根的所有子树块带来的答案的贡献
			ans-=1ll*(nex-pre)*(nex-pre-1)/2;
		}
		printf("Case #%d: %lld\n",++ca,ans);
	}
	return 0;
} 

HDU 6038 Function(置换群 循环节)

多组样例,保证n和m总和不超过1e6

每组样例,给出一个n和m(1<=n,m<=1e5),

给出长度为n的a[],0<=ai<=n-1,下标为0到n-1

给出长度为m的b[],0<=bj<=m-1,下标为0到m-1

输出不同的映射f数量,对于每个i,都满足f(i)=b_{f(a_{i})},方案模1e9+7

 

手跑样例可以发现,置换群是一个个独立的环,

n个点n条边,所以必有至少一个环;去掉这一个环,点边仍相等,所以皆为环

b中的数对应一个个长度的环cyc1,而映射关系对应a中的数的环cyc2,

应满足cyc2是cyc1的倍数,起点从cyc1的对应的|cyc1|个数中任取一个

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
typedef long long ll;
const int mod=1e9+7;
const int N=1e5+10;
int ca,n,m,a[N],b[N];
bool vis[N],vis2[N];
int cyc[N];//cyc[i]:存在长度为i的循环节cyc[i]个 
vector<int>ans; 
void add(ll &x,ll y)
{
	x+=y;
	if(x<0)x+=mod;
	if(x>=mod)x-=mod;
}
void init()
{
	for(int i=0;i<=m;++i)
	cyc[i]=vis[i]=0;
	for(int i=0;i<=n;++i)
	vis2[i]=0;
	ans.clear();
}
void solve()
{
	ll res=1;
	for(int i=0;i<m;++i)
	{
		if(vis[i])continue;
		int cnt=0;
		for(int j=i;!vis[j];j=b[j])
		{
			cnt++;
			vis[j]=1;
		}
		cyc[cnt]++;
		//printf("cyc:%d\n",cnt);
	}
	for(int i=0;i<n;++i)
	{
		if(vis2[i])continue;
		int cnt=0;
		for(int j=i;!vis2[j];j=a[j])
		{
			cnt++;
			vis2[j]=1;
		}
		ans.push_back(cnt);
	}
	for(int v:ans)
	{
		//printf("v:%d\n",v);
		ll tmp=0;
		for(int j=1;j*j<=v;++j)
		{
			if(v%j==0)
			{
				add(tmp,1ll*cyc[j]*j%mod);
				if(j!=v/j)add(tmp,1ll*cyc[v/j]*(v/j)%mod);
			}
		}
		//printf("tmp:%lld\n",tmp);
		res=res*tmp%mod;
	}
	printf("Case #%d: %lld\n",++ca,res);
}
int main()
{
	while(~scanf("%d%d",&n,&m))
	{
		for(int i=0;i<n;++i)
		scanf("%d",&a[i]);
		for(int i=0;i<m;++i)
		scanf("%d",&b[i]);
		solve();
		init();
	}
	return 0;
}

HDU 6040 Hints of sd0061(nth_element)

多组样例,约15组,

每组样例,给出一个n(n<=1e7),由rng61()生成长度为n的数组a[],

m(m<=100)个询问,第j次询问数组a[]中从小到大rank=bj的值

允许离线,保证对于任意bi,bj,bk,若bi<bk,bj<bk,则bi+bj<=bk

 

STL中nth_element函数的应用,其实就是快排的思想,

每次找到轴中值之后,只搜另一半,确定这个数的rank期望意义下,复杂度O(n)

nth_element(start,rank,end),三个参数都为a+偏移量的形式

rank就是快排中的轴,即调用该函数后,rank位置为第rank的值v,左侧均为<v的值,右侧均为>v的值

 

b给定的形式,类似斐波那契数列,bj<=bk/2,保证排序之后至少降一半

所以对于连续增序三元组(i,j,k),在[b,b+k)里询问到bj之后,下次询问bi,只需在[b,b+j)即可

最坏情况n,n/2,n/4,复杂度O(2*n),即O(n)

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=1e7+10;
typedef unsigned u;
int n,m;
u A,B,C,ans[N],a[N];
u x,y,z;
struct node
{
	int id,rk;	
}e[105];
bool operator<(node a,node b)
{
	return a.rk<b.rk;
}
unsigned rng61()
{
	unsigned t;
	x = x ^ (x << 16);
	x = x ^ (x >> 5);
	x = x ^ (x << 1);
	t = x;
	x = y;
	y = z;
	z = (t ^ x) ^ y;
	return z;
}
int main()
{
	int ca=0;
	while(~scanf("%d%d%u%u%u",&n,&m,&A,&B,&C))
	{
		x = A, y = B, z = C;
		for(int i=0;i<n;++i)
		a[i]=rng61();
		for(int i=0;i<m;++i)
		{
			scanf("%d",&e[i].rk);
			e[i].id=i;
		}
		sort(e,e+m);
		e[m].rk=n;
		for(int i=m-1;i>=0;--i)
		{
			//nth_element(start,rk,end) 使rk归位的操作 
			nth_element(a,a+e[i].rk,a+e[i+1].rk);
			ans[e[i].id]=a[e[i].rk]; 
		}
		printf("Case #%d:",++ca);
		for(int i=0;i<m;++i)
		printf(" %u",ans[i]);
		puts("");
	}
	return 0;
} 

HDU 6041 I Curse Myself(仙人掌图求环+第k大和)

多组样例,每次给定一张n(n<=1e3)个点,m(m<=2n-3)的边的仙人掌,边权w<=1e6

\sum k*V(k),其中V(k)为从小到大第k生成树的权值和,边权相同但树形不同视作两个

 

考虑仙人掌图,每个环独立,即每个环拆一条边,

最小生成树=权值总和-最大拆边和

把每个环扒下来排序构成一个集合,不妨共num个环,

问题转化成num个集合中,每个集合选取一个数,所组成的从大到小1到k的权值和

经典问题,如poj2442、uva11997(https://blog.csdn.net/Code92007/article/details/91906189)

复杂度相比少log,证明用以下不等式

空间要求比较严格,要求用滚动数组

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
#include<queue>
using namespace std;
typedef unsigned un;
const int N=1e3+10; 
const int M=2e3+10;
const int K=1e5+10;
struct edge
{
	int v,nex,w;
}e[M*2];
struct node
{
	int v,pos;
}f[N];
bool operator<(node a,node b)
{
	return a.v<b.v;
}
int n,m,k,u,v,w;
int head[N],cnt;
int dfn[N],c[N],tot,num;
int sz[2],ans[2][K];
vector<int>cyc[N];
un res,sum;
priority_queue<node>q;
void add(int u,int v,int w)
{
	e[++cnt]=edge{v,head[u],w};
	head[u]=cnt;
}
void init()
{
	memset(head,0,sizeof head);
	memset(ans,0,sizeof ans);
	memset(dfn,0,sizeof dfn);
	for(int i=1;i<=num;++i)
	cyc[i].clear();
	res=sum=num=cnt=tot=0;
}
void dfs(int u,int fa)
{
	dfn[u]=++tot;
	for(int i=head[u];i;i=e[i].nex)
	{
		int v=e[i].v,w=e[i].w;
		if(dfn[v]==-1||v==fa)continue;
		c[tot]=w;
		if(dfn[v])
		{
			num++;
			for(int j=dfn[u];j>=dfn[v];j--)
			cyc[num].push_back(c[j]);
		}
		else dfs(v,u); 
	}
	tot--;
	dfn[u]=-1;
}
void merge(vector<int> &x,int f,int *pre,int *now)
{
	while(!q.empty())q.pop();
	for(int i=0;i<x.size();++i)
	q.push(node{x[i]+pre[1],1});//(v,nowid) 
	node t;
	for(int i=1;i<=k;++i)
	{
		if(q.empty())break;
		t=q.top();q.pop();
		int v=t.v,pos=t.pos;
		sz[f]=i;
		now[i]=v;
		if(pos+1<=sz[f^1])q.push(node{v-pre[pos]+pre[pos+1],pos+1});
	}
}
int main()
{
	int ca=0;
	while(~scanf("%d%d",&n,&m))
	{
		init();
		for(int i=1;i<=m;++i)
		{
			scanf("%d%d%d",&u,&v,&w);
			add(u,v,w);add(v,u,w); 
			sum+=w;
		}
		scanf("%d",&k);
		dfs(1,-1);
		sz[0]=1;//有一个0 作初值 
		for(int i=1;i<=num;++i)
		{
			sort(cyc[i].begin(),cyc[i].end(),greater<int>());
			merge(cyc[i],i&1,ans[(i-1)&1],ans[i&1]);
		}
		//V(k)==0 if k-th spanning tree doesn't exist 
		for(un i=1;i<=sz[num&1];++i)
		res+=i*(sum-(un)ans[num&1][i]);
		printf("Case #%d: %u\n",++ca,res);
	} 
	return 0;
} 

HDU 6044 Limited Permutation(组合数学+dfs)

多组样例,n(n<=1e6)个数,要你设计一个1到n的排列,

使得第i个数在给定的[li,ri]中是最小值,求方案数

 

考虑有一个区间一定是[1,n],设这是第i个数,则第i个数一定填1

递归考虑[1,i-1]和[i+1,n]填什么,记左区间长度为l,右区间长度为r

f(all)=C_{l+r}^{l}*f(l)*f(r),先挑一些数给左边,剩下给右边

类似线段树维护的区间,[l,r]有两个子树[l,mid]和[mid+1,r],应按先序dfs这棵树

巧妙地发现,按左端点排增序,左端点相同时,再按右端点排降序,即可实现

注意无方案的几种情形及递归终点

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int mod=1e9+7; 
const int maxn=1e6;
const int N=maxn+5;
int ca,n,now;
bool ok;
ll Finv[N],jc[N];
ll modpow(ll x,ll n,ll mod)
{
	ll res=1;
	for(;n;x=x*x%mod,n/=2)
	if(n&1)res=res*x%mod;
	return res;
}
void init()
{
	jc[0]=Finv[0]=1;
	for(int i=1;i<=maxn;++i)
	{
	 jc[i]=jc[i-1]*i;
	 if(jc[i]>=mod)jc[i]%=mod;
    }
	Finv[maxn]=modpow(jc[maxn],mod-2,mod);
	for(int i=maxn-1;i>=1;--i)
	{
	 Finv[i]=Finv[i+1]*(i+1);
	 if(Finv[i]>=mod)Finv[i]%=mod;
    }
}
ll C(ll n,ll m)
{
	if(m<0||m>n)return 0;
	return jc[n]*Finv[n-m]%mod*Finv[m]%mod;
}
struct node
{
	int l,r,pos;
}e[N];
bool operator<(node a,node b)
{
	if(a.l==b.l)return a.r>b.r;
	return a.l<b.l;
}
ll dfs(int l,int r)
{
	if(!ok)return 0;
	if(l>r)return 1;
	++now;
	if(e[now].l!=l||e[now].r!=r||e[now].pos>r||e[now].pos<l)return ok=0;
	ll res=1;
	int pos=e[now].pos;
	res=C(r-l,pos-l)*dfs(l,pos-1)%mod;
	res=res*dfs(pos+1,r)%mod;
	return res; 
}
int main()
{
	init();
	while(~scanf("%d",&n))
	{
		ok=1;now=0;
		for(int i=1;i<=n;++i)
		scanf("%d",&e[i].l);
		for(int i=1;i<=n;++i)
		{
			scanf("%d",&e[i].r);
			e[i].pos=i;
		}
		sort(e+1,e+n+1);
		printf("Case #%d: %lld\n",++ca,dfs(1,n));
	}
	return 0;
} 

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Code92007

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值