contest:10.22

猴猴吃苹果(分治,贪心)

猴猴吃苹果

题目大意:定义树上两点的距离为两点之间未访问的点的数量. 每次求距离已访问的点集最远的叶子结点,然后将该叶子到点集的路径上的点全部设为已访问. 求访问叶子的顺序.

60pts:看到求区间点权和修改区间点权,首先考虑 O ( n 2 l o g 2 n ) O(n^2log^2n) O(n2log2n)的树链剖分,当然也可以采用 O ( n 3 ) O(n^3) O(n3) s p f a spfa spfa(时间复杂度正确性请求)或者爆搜.

在这里给出树链剖分的算法.

#include<bits/stdc++.h>
#define maxn 50010
#define mid ((l+r)>>1)
using namespace std;
struct edge{
	int v,next;
	}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
	e[++tot].v=v;
	e[tot].next=head[u];
	head[u]=tot;
	}
int ans[maxn<<3],tag[maxn<<3];
void push_up(int p){ans[p]=ans[p<<1]+ans[p<<1|1];}
void build(int p,int l,int r){
	if(l==r){
		ans[p]=1;
		tag[p]=-1;
		return ;
		}
	tag[p]=-1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	push_up(p);
	}
void push_down(int p){
	if(tag[p]==-1)return ;
	ans[p<<1]=0,ans[p<<1|1]=0;
	tag[p<<1]=0,tag[p<<1|1]=0;
	tag[p]=-1;
	}
void update(int p,int l,int r,int ll,int rr){
	if(ll<=l&&r<=rr){
		ans[p]=0;
		tag[p]=0;
		return ;
		}
	push_down(p);
	if(ll<=mid)update(p<<1,l,mid,ll,rr);
	if(mid<rr)update(p<<1|1,mid+1,r,ll,rr);
	push_up(p);
	}
int query(int p,int l,int r,int ll,int rr){
	if(ll<=l&&r<=rr){
		return ans[p];
		}
	int ret=0;
	push_down(p);
	if(ll<=mid)ret+=query(p<<1,l,mid,ll,rr);
	if(mid<rr)ret+=query(p<<1|1,mid+1,r,ll,rr);
	return ret;
	}
int fa[maxn],size[maxn],son[maxn],dep[maxn];
void dfs1(int u,int pre){
	fa[u]=pre;
	size[u]=1;
	dep[u]=dep[pre]+1;
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==pre)continue;
		dfs1(v,u);
		size[u]+=size[v];
		if(size[v]>size[son[u]])son[u]=v;
		}
	}
int cnt,id[maxn],top[maxn],num;
vector<int >g;
void dfs2(int u,int topu){
	id[u]=++cnt;
	top[u]=topu;
	if(!son[u]){
		num++;
		g.push_back(u);
		return ;
		}
	dfs2(son[u],topu);
	for(int i=head[u];i;i=e[i].next){
		int v=e[i].v;
		if(v==fa[u]||v==son[u])continue;
		dfs2(v,v);
		}
	}
int n,s;
void update_path(int x,int y){
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		update(1,1,n,id[top[x]],id[x]);
		x=fa[top[x]];
		}
	if(dep[x]>dep[y])swap(x,y);
	update(1,1,n,id[x],id[y]);
	}
int query_path(int x,int y){
	int ret=0;
	while(top[x]!=top[y]){
		if(dep[top[x]]<dep[top[y]])swap(x,y);
		ret+=query(1,1,n,id[top[x]],id[x]);
		x=fa[top[x]];
		}
	if(dep[x]>dep[y])swap(x,y);
	ret+=query(1,1,n,id[x],id[y]);
	return ret;
	}
int main(){
	//freopen("apple.in","r",stdin);
	//freopen("apple.out","w",stdout);
	scanf("%d%d",&n,&s);s++;
	for(int i=2,u;i<=n;i++){
		scanf("%d",&u);
		add(u+1,i);add(i,u+1);
		}
	dfs1(s,s);
	dfs2(s,s);
	build(1,1,n);
	printf("%d\n",s-1);
	for(int i=1,node=0,sum=0,mx=0;i<=num;i++){
		mx=0,sum=0,node=0;
		for(long long j=0;j<g.size();j++){
			sum=query_path(s,g[j]);
			if(sum>mx||(sum==mx&&g[j]<node)){
				mx=sum;
				node=g[j];
				}
			}
			
			printf("%d\n",node-1);
			update_path(s,node);
		}
		return 0;
	}

100pts:

1.首先要根据各个叶子距离原点 s s s的距离排序(第一次排序).
2.那么,第一次排序之后第一个访问的叶子就确定了.
3.考虑访问的第二个叶子,有两种情况:
其一,该叶子到根的路径上的点全部没有被访问过,那么显然应该选这个叶子;
其二,该叶子到根的路径上的点有一部分被访问过,那么我们要证明:选这个叶子不会影响第二次排序后的正确性.

在这里我们重点考虑第二种情况.

我们把这个叶子的深度最小的未访问的祖先的子树拿出来单独讨论.

在这棵子树上这个叶子依然是最远的,这就保证了局部的答案正确,因此对于每个叶子,我们都可以如是操作. 也就是说在这里这个叶子现在指定给它的访问顺序不会影响这棵子树内部的访问顺序,对于每棵子树,当前的访问顺序都是满足条件的,所以我们要选择它.

不严谨的说,这就相当于我们把树一分为多,对于这若干棵子树分别进行操作,求出每个子树的局部答案,最后再排一次序. 以保证全局答案正确.


那么整体的做法就是:

1.第一次dfs,然后按深度和子节点编号对叶子进行排序.
2.按照第一次排序的顺序对从叶子到已访问点集的路径标记访问并记录长度.
3.按到点集的距离对叶子节点再次排序.


几个注意的点:

1.题目中树的编号从0开始,但是为了方便处理通常会从1开始编号. 输出时记得减回来.
2.sort的理论时间复杂度虽然是 O ( n l o g n ) O(nlogn) O(nlogn),但是实际中体现的时间复杂度是 O ( n k ) O(nk) O(nk)(k为小常数).

#include<bits/stdc++.h>
#define maxn 50010
using namespace std;
struct edge{
	int v,next;
}e[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
struct node{
	int pos,dep,dis;
	bool operator <(node x)const{
		if(dep==x.dep )return pos<x.pos ;
		return dep>x.dep ;
	}
}nd[maxn];
int cnt,fa[maxn];
bool vis[maxn];
void dfs(int u,int pre,int dep){
	bool flag=false;fa[u]=pre;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs(v,u,dep+1);flag=true;
	}
	if(!flag){
		nd[++cnt].dep =dep;
		nd[cnt].pos =u;
	}
}
bool cmp(node x,node y){
	if(x.dis ==y.dis )return x.pos <y.pos ;
	return x.dis >y.dis ;
}
int n,s;
int main(){
	scanf("%d%d",&n,&s);s++;
	for(int i=2,u;i<=n;i++){
		scanf("%d",&u);
		add(u+1,i);add(i,u+1);
	}
	dfs(s,s,1);
	sort(nd+1,nd+cnt+1);
	printf("%d\n",s-1);
	for(int i=1;i<=cnt;i++){
		int u=nd[i].pos ;
		while(!vis[u]){
			vis[u]=true;
			u=fa[u];
			nd[i].dis ++;
		}
	}
	sort(nd+1,nd+cnt+1,cmp);
	for(int i=1;i<=cnt;i++)printf("%d\n",nd[i].pos -1);
	return 0;
} 

猴猴吃香蕉(01背包,因数分解)

猴猴吃香蕉

60pts:首先,每个数都可以选择或者不选择,而最后他们的乘积为一个数,有前后转移的关系,又符合无后效性和最优化原理,所以这是一个01背包问题.

状态设计: f [ i ] f[i] f[i]表示到当前所有数的乘积为 i i i的方案数.

状态转移方程: f [ i ] = ∑   f [ i / a [ j ] ] , 1 < = j < = n f[i]=\sum\:f[i/a[j]],1<=j<=n f[i]=f[i/a[j]],1<=j<=n.

#include<bits/stdc++.h>
#define maxn 10010
using namespace std;
int n,m,a[maxn],f[maxn];
const int mod=1e9+7;
int main(){
	int T;scanf("%d",&T);
	while(T--){
		memset(f,0,sizeof f);
		scanf("%d%d",&n,&m);
		for(int i=1;i<=n;i++)scanf("%d",&a[i]);
		f[1]=1;
		for(int i=1;i<=n;i++){
			for(int j=m;j>=a[i];j--){
				if(j%a[i]==0){
					f[j]=(f[j]+f[j/a[i]])%mod;
				}
			}
		}
		printf("%d\n",f[m]);
	}
	return 0;
}

100pts:

不妨考虑为什么上面的程序会超时&runtime error.

第一,我们的数组开不到k的值的大小—— 1 e 8 1e8 1e8;第二,我们计算时两重循环的时间复杂度为 O ( n k ) O(nk) O(nk).

而我们可以发现以上的程序实际上计算了大量的无用状态,比如说有一个状态(乘积)为x,而 g c d ( x , k ) = 1 gcd(x,k)=1 gcd(x,k)=1,那么这个x状态是无效的,不难发现我们需要的状态只有那些属于 k k k的因数的状态,因为只有这些状态才能转移到状态 k k k.

所以我们第二重循环只需要处理 k k k为数不多的因数的转移就行了.

状态转移方程: f [ i ] = ∑   f [ i / a [ j ] ] , 1 < = j < = n , i f[i]=\sum\:f[i/a[j]],1<=j<=n,i f[i]=f[i/a[j]],1<=j<=n,i k k k的因数.

时间复杂度降下来了,但是空间复杂度还是太大,究其原因,空间也有大量无效占用.

解决办法:离散化. 预处理出 k k k的所有因数,然后排序,离散化,状态转移时就用离散化后的数组转移.

状态设计: f [ i ] f[i] f[i]表示之前所有数的乘积等于第i个因数的方案数. 其中因数已排序去重.

离散化可以用 m a p map map或者 s o r t + ( u n i q u e ) + l o w e r b o u n d sort+(unique)+lower_bound sort+(unique)+lowerbound,但是后者比前者快三倍.

这里放上简单易懂的第二种方法.

#include<bits/stdc++.h>
#define maxn 10010
using namespace std;
const int mod =1e9+7;
int cnt,sum[maxn];
void divide(int x){
	for(int i=1;i*i<=x;i++){
		if(x%i==0){
			sum[++cnt]=i;
			if(x/i!=i)sum[++cnt]=x/i;
		}
	}
	sort(sum+1,sum+cnt+1);
	//排序,因为不会有重复的情况所以无需去重
}//以上为因数分解
int n,m,a[maxn],f[maxn];
int main(){
	int T;scanf("%d",&T);
	while(T--){
		memset(sum,0,sizeof sum);
		memset(f,0,sizeof f);
		memset(a,0,sizeof a);cnt=0;
		scanf("%d%d",&n,&m);
		divide(m);
		f[1]=1;//初始状态(通用设法)
		for(int i=1;i<=n;i++)scanf("%d",&a[i]);
		for(int i=1;i<=n;i++){
			for(int j=cnt;j;j--){
				if(sum[j]>=a[i]&&sum[j]%a[i]==0){//如果可以转移
					int pos=lower_bound(sum+1,sum+cnt+1,sum[j]/a[i])-sum;
					//获取该被转移状态(同样是一个因数)的下标(第pos个)作为实际上数组中的状态
					f[j]=(f[j]+f[pos])%mod;
				}
			}
		}
		printf("%d\n",f[cnt]);
	}
	return 0;
} 

顺带讲一句背包问题的初始化:基本上都是仅初始化一个数,比如常见的背包,初始化 f [ 0 ] = 0 f[0]=0 f[0]=0表示容量为0时的价值可以为0.

当然这里还会涉及到“恰好装满”的初始化问题,恰好装满其他就都初始化为 − i n f -inf inf,不用恰好装满其他就可以初始化为 0 0 0,这个在背包九讲有说.

而这里的 f [ 1 ] = 1 f[1]=1 f[1]=1表示乘积为1时方案数为1,这个也属于比较常见、套路的一个初始化,要加以理解.


猴猴的比赛(dfs序,树状数组)

猴猴的比赛

题目大意: 求满足条件的点集 ( a , b ) (a,b) (a,b)的数量. 条件如下:在给定的两棵树中,都有 l c a a , b = a lca_{a,b}=a lcaa,b=a d e p a < d e p b dep_a<dep_b depa<depb.

60pts:考虑暴力算法,对于一棵树,每个点开一个 v e c t o r vector vector存它的所有子树内的节点,然后在第二棵树中同样开一个 v e c t o r vector vector存,如果一个元素在两个 v e c t o r vector vector中均有出现那么 a n s + + ans++ ans++. 时间复杂度 O ( n 2 l o g   n ) O(n^2log\:n) O(n2logn)或者 O ( n 3 ) O(n^3) O(n3)(非常抱歉我不确定find的时间复杂度是多少).

#include<bits/stdc++.h>
#define maxn 100010
using namespace std;
struct edge{
	int v,next;
}e[maxn<<1],p[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
int fir[maxn],cnt;
void ad(int u,int v){
	p[++cnt].v =v;
	p[cnt].next =fir[u];
	fir[u]=cnt;
}
vector<int >g[maxn],f[maxn];
void dfs1(int u,int pre){
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u);g[u].push_back(v);
		g[u].insert(g[u].begin(),g[v].begin(),g[v].end());
	}
}
int ans;
void dfs2(int u,int pre){
	for(int i=fir[u];i;i=p[i].next ){
		int v=p[i].v ;
		if(v==pre)continue;
		dfs2(v,u);f[u].push_back(v);
		f[u].insert(f[u].begin(),f[v].begin(),f[v].end());
	}
	vector<int >::iterator it;
	for(int i=0;i<f[u].size();i++){
		int v=f[u][i];
		it=find(g[u].begin(),g[u].end(),v);
		if(it==g[u].end())continue;//对于每一个元素,查找它是否在两个vector中都出现了
		else ans++;
	}
}
int n;
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs1(1,1);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		ad(u,v);ad(v,u);
	}
	dfs2(1,1);
	printf("%d\n",ans);
	return 0;
} 

100pts:由于这是一个和深度有关的问题,所以不妨考虑使用dfs序来表示深度,把树摊平成一个区间.

我们对第一棵树进行求dfs序的操作,用一个树状数组维护这个 d f s dfs dfs序,然后在遍历第二棵树的时候,对于每个点,我们都求一下在这个点dfs序的区间内的和,每个点的这个和累加就是最终的答案. 在求完这个点的所有子树之后,再给这个点在dfs序上对应的位置++.

在这里插入图片描述
举个例子,比如访问到第二棵树的2时,

dfs序12344321
区间上对应的数00111100

那么满足条件的数对 ( 2 , x ) (2,x) (2,x)的数量就是 4 / 2 = 2 4/2=2 4/2=2.

我们在求完 2 2 2之后递归回 1 1 1,此时

dfs序12344321
区间上对应的数01111110

那么满足条件的数对 ( 1 , x ) (1,x) (1,x)的数量就是 6 / 2 = 3 6/2=3 6/2=3.

但是注意,在遍历树的时候,我们可能先遍历4,再遍历到3,此时的序列

dfs序12344321
区间上对应的数00011000

这样点对 ( 3 , x ) (3,x) (3,x)的数量应该为1,但是显然满足条件的点对是不存在的.

所以在每一次计算一个点的所有子树之后,需要把这个子树上所有点对dfs序的影响都清零,在计算完这个点之后再还原.

如此,才能防止因为其他子树导致类似上面的统计错误.


时间复杂度为 O ( n   l o g   n ) O(n\:log\:n) O(nlogn).

#include<bits/stdc++.h>
#define maxn 100010
using namespace std;
struct edge{
	int v,next;
}e[maxn<<1],p[maxn<<1];
int head[maxn],tot;
void add(int u,int v){
	e[++tot].v =v;
	e[tot].next =head[u];
	head[u]=tot;
}
int fir[maxn],cnt;
void ad(int u,int v){
	p[++cnt].v =v;
	p[cnt].next =fir[u];
	fir[u]=cnt;
}
int rg[maxn][2];
int a[maxn<<1],tree[maxn<<1];
int sum;
int lowbit(int x){return x&-x;}
void update(int x,int k){
	for(int i=x;i<=sum;i+=lowbit(i))tree[i]+=k;
} 
int query(int x){
	int ret=0;
	for(int i=x;i;i-=lowbit(i))ret+=tree[i];
	return ret;
}
void dfs1(int u,int pre){
	a[++sum]=u;rg[u][0]=sum;
	for(int i=head[u];i;i=e[i].next ){
		int v=e[i].v ;
		if(v==pre)continue;
		dfs1(v,u);
	}
	a[++sum]=u;rg[u][1]=sum;
}
int ans;
void dfs2(int u,int pre,int k){
	if(k==0){
	//k==0时进行常规操作,k==1或k==-1时进行区间还原操作
		for(int i=fir[u];i;i=p[i].next ){
			int v=p[i].v ;
			if(v==pre)continue;
			dfs2(v,u,0);
			ans+=query(rg[u][1])-query(rg[u][0]-1);
			dfs2(v,u,-1);//还原这个子树对dfs序造成的影响
		}
		update(rg[u][0],1);
		update(rg[u][1],1);
		for(int i=fir[u];i;i=p[i].next ){
			int v=p[i].v ;
			if(v==pre)continue;
			dfs2(v,u,1);//已算出结果,还原整个dfs序
		}
	return ;
	}
	for(int i=fir[u];i;i=p[i].next ){
		int v=p[i].v ;
		if(v==pre)continue;
		dfs2(v,u,k);
	}
	update(rg[u][0],k);
	update(rg[u][1],k);
}
int n;
int main(){
	scanf("%d",&n);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		add(u,v);add(v,u);
	}
	dfs1(1,1);
	for(int i=1,u,v;i<n;i++){
		scanf("%d%d",&u,&v);
		ad(u,v);ad(v,u);
	}
	dfs2(1,1,0);
	printf("%d\n",ans/2);
	return 0;
} 
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值