HAOI 2016 地图 题解

本文探讨了一道关于仙人掌图的算法问题,通过构建圆方树和使用树状数组来统计每种拉面油腻度的出现次数,从而解决从特定节点出发能吃到的拉面油腻度中奇数或偶数次的种类数量。

题目传送门

题目大意: 给一张仙人掌图,多组询问,每次给出一个点,询问封掉点 1 1 1 到这个点的所有路径后,从这个点出发能吃到的拉面的油腻度中吃了奇数次或偶数次的油腻度有多少种。

题解

纪念一下看错题后乱JB敲出来的代码(放心,这东西和题解没有半毛钱关系)。

#include <cstdio>
#include <map>
#include <algorithm>
using namespace std;
#define maxn 200010

int n,m,q,nn,a[maxn];
struct V_E{
	struct edge{int y,type,id,next;};
	edge e[maxn<<1];
	int first[maxn],len;
	void buildroad(int x,int y,int type=0,int id=0)
	{
		e[++len]=(edge){y,type,id,first[x]};
		first[x]=len;
	}
}T1,T2,T3;
int tr[maxn<<3],odd[maxn],even[maxn];
int lowbit(int x){return x&(-x);}
void tr_add(int *tree,int x,int y){for(;x<=1000000;x+=lowbit(x))tree[x]+=y;}
int tr_sum(int *tree,int x){int p=0;for(;x>=1;x-=lowbit(x))p+=tree[x];return p;}
int tr_ask(int *tree,int x){return tr_sum(tree,x)-tr_sum(tree,x-1);}
void add(int x)
{
	int p=tr_ask(tr,x); tr_add(tr,x,1);
	if(p==0)tr_add(odd,x,1);
	else if(p%2==0)tr_add(even,x,-1),tr_add(odd,x,1);
	else tr_add(odd,x,-1),tr_add(even,x,1);
}
void del(int x)
{
	int p=tr_ask(tr,x); tr_add(tr,x,1);
	if(p==1)tr_add(odd,x,-1);
	else if(p%2==0)tr_add(even,x,-1),tr_add(odd,x,1);
	else tr_add(odd,x,-1),tr_add(even,x,1);
}
int dfn[maxn],low[maxn],id=0,fa[maxn],belong[maxn];
void solve(int x,int y)
{
	int now=y; nn++;
	while(now!=fa[x])belong[now]=nn,T2.buildroad(now,nn),T2.buildroad(nn,now),now=fa[now];
}
void tarjan(int x)
{
	dfn[x]=low[x]=++id;
	for(int i=T1.first[x];i;i=T1.e[i].next)
	{
		int y=T1.e[i].y; if(y==fa[x])continue;
		if(!dfn[y])
		{
			fa[y]=x; tarjan(y);
			if(low[y]<low[x])low[x]=low[y];
		}
		else if(dfn[y]<low[x])low[x]=dfn[y];
		if(low[y]<=dfn[x])continue;
		T2.buildroad(x,y);T2.buildroad(y,x);
	}
	for(int i=T1.first[x];i;i=T1.e[i].next)
	{
		int y=T1.e[i].y; if(fa[y]==x||dfn[y]<dfn[x])continue;
		solve(x,y);
	}
}
int ans[maxn];
bool vis[maxn];
void work(int x,int type)
{
	if(vis[x])return;
	if(belong[x])
	{
		x=belong[x]; vis[x]=true;
		for(int i=T2.first[x];i;i=T2.e[i].next)
		vis[T2.e[i].y]=true,del(a[T2.e[i].y]);
		if(type==1)
		{
			for(int i=T2.first[x];i;i=T2.e[i].next)
			{
				for(int i=T3.first[T2.e[i].y];i;i=T3.e[i].next)
				if(T3.e[i].type==0)ans[T3.e[i].id]=tr_sum(even,T3.e[i].y);
				else ans[T3.e[i].y]=tr_sum(odd,T3.e[i].y);
			}
		}
	}
	else
	{
		del(a[x]); vis[x]=true;
		if(type==1)
		{
			for(int i=T3.first[x];i;i=T3.e[i].next)
			if(T3.e[i].type==0)ans[T3.e[i].id]=tr_sum(even,T3.e[i].y);
			else ans[T3.e[i].y]=tr_sum(odd,T3.e[i].y);
		}
	}
}
void go(int x,int father)
{
	work(x,1);
	for(int i=T2.first[x];i;i=T2.e[i].next)
	if(T2.e[i].y!=father)go(T2.e[i].y,x);
	work(x,-1);
}
struct par{
	int x,y;
	par(int xx,int yy):x(xx),y(yy){}
	bool operator <(const par &b)const{return x==b.x?(y<b.y):(x<b.x);}
};
map<par,bool> Map;

int main()
{
	scanf("%d %d",&n,&m); nn=n;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]),add(a[i]);
	for(int i=1,x,y;i<=m;i++)
	{
		scanf("%d %d",&x,&y);
		if(!Map[par(x,y)])T1.buildroad(x,y),T1.buildroad(y,x),Map[par(x,y)]=true;
	}
	scanf("%d",&q);
	for(int i=1,type,x,y;i<=q;i++)
	{
		scanf("%d %d %d",&type,&x,&y);
		T3.buildroad(x,y,type,i);
	}
	tarjan(1); go(1,0);
	for(int i=1;i<=q;i++)printf("%d\n",ans[i]);
}

真好看不是吗qwq

发现这张图是一张仙人掌,那么自然想到以点 1 1 1 为根造一棵圆方树,然后题目就转化成了询问某个点子树内出现次数为奇数次或偶数次的油腻度次数。

好像突然就简单了很多?

拉一拉 dsu on tree 的板子即可。在统计的时候开一个一百万大小的树状数组维护一下每种颜色的出现次数,再开两个树状数组分别维护每种颜色出现了偶数次还是奇数次,理论时间复杂度 O ( n l o g 2 n ) O(nlog^2n) O(nlog2n)

由于常数有点大所以跑的并没有 O ( n n ) O(n\sqrt n) O(nn ) 的分块快qaq,但是吸口氧比原来快很多,能跑出 1.8 s 1.8s 1.8s 的好成绩~

还有什么不懂的就看代码吧,注释很详细呢。

updata 当日: 突然想到那个一百万大小的树状数组我居然在用来改点求点??所以那个可以直接用数组代替……然后吸了氧就跑到 1.5 s 1.5s 1.5s了!

代码如下:

#include <cstdio>
#include <map>
#include <algorithm>
using namespace std;
#define maxn 1000010

int n,m,q,nn,a[maxn];
struct par{
	int x,y;
	par(int xx,int yy):x(xx),y(yy){}
	bool operator <(const par &b)const{return x==b.x?(y<b.y):(x<b.x);}
};
map<par,bool> Map;
struct V_E{
	struct edge{int y,type,id,next;};//type和id只有T3用到,下面会讲 
	edge e[maxn<<1];
	int first[maxn],len;
	void buildroad(int x,int y,int type=0,int id=0)
	{
		e[++len]=(edge){y,type,id,first[x]};
		first[x]=len;
	}
}T1,T2,T3;
//T1是原图,T2记录圆方树,T3用来记录询问
//T3中的y存的就是询问的y,id存这是哪个询问,type存 问的是奇数还是偶数 
int times[maxn],odd[maxn],even[maxn];
//times记录出现次数,odd和even是树状数组
//如果一个油腻度出现奇数次,那么odd对应位置+1,偶数次就让even对应位置+1
//询问小于等于某个油腻度有多少种油腻度出现奇数或偶数次是求个前缀和即可 
int lowbit(int x){return x&(-x);}
void tr_add(int *tree,int x,int y){for(;x<=1000000;x+=lowbit(x))tree[x]+=y;}//树状数组单点修改 
int tr_sum(int *tree,int x){int p=0;for(;x>=1;x-=lowbit(x))p+=tree[x];return p;}//树状数组求前缀和 
int tr_ask(int *tree,int x){return tr_sum(tree,x)-tr_sum(tree,x-1);}//树状数组单点求值 
void add(int x)//油腻度x出现次数+1 
{
	if(!x)return;
	times[x]++;
	if(times[x]==1)tr_add(odd,x,1);
	else if(times[x]%2==1)tr_add(even,x,-1),tr_add(odd,x,1);
	else tr_add(odd,x,-1),tr_add(even,x,1);
}
void del(int x)//油腻度x出现次数-1 
{
	if(!x)return;
	times[x]--;
	if(times[x]==0)tr_add(odd,x,-1);
	else if(times[x]%2==1)tr_add(even,x,-1),tr_add(odd,x,1);
	else tr_add(odd,x,-1),tr_add(even,x,1);
}
int dfn[maxn],low[maxn],id=0,fa[maxn],belong[maxn];
void solve(int x,int y)//圆方树板子 
{
	int now=y; nn++;
	while(now!=fa[x])belong[now]=nn,T2.buildroad(now,nn),T2.buildroad(nn,now),now=fa[now];
}
void tarjan(int x)//圆方树板子 
{
	dfn[x]=low[x]=++id;
	for(int i=T1.first[x];i;i=T1.e[i].next)
	{
		int y=T1.e[i].y; if(y==fa[x])continue;
		if(!dfn[y])
		{
			fa[y]=x; tarjan(y);
			if(low[y]<low[x])low[x]=low[y];
		}
		else if(dfn[y]<low[x])low[x]=dfn[y];
		if(low[y]<=dfn[x])continue;
		T2.buildroad(x,y);T2.buildroad(y,x);
	}
	for(int i=T1.first[x];i;i=T1.e[i].next)
	{
		int y=T1.e[i].y; if(fa[y]==x||dfn[y]<dfn[x])continue;
		solve(x,y);
	}
}
int size[maxn],mson[maxn];
int ans[maxn];
void dfs1(int x,int fa)//找重儿子 
{
	size[x]=1;
	for(int i=T2.first[x];i;i=T2.e[i].next)
	{
		int y=T2.e[i].y; if(y==fa)continue;
		dfs1(y,x); size[x]+=size[y];
		if(size[y]>size[mson[x]])mson[x]=y;
	}
}
void go(int x,int fa,bool tf)//加上/删去 以x为根的子树的贡献 
{
	if(tf)add(a[x]); else del(a[x]);
	for(int i=T2.first[x];i;i=T2.e[i].next)
	if(T2.e[i].y!=fa)go(T2.e[i].y,x,tf);
}
void getans(int x)//求解所有对x的询问 
{
	for(int i=T3.first[x];i;i=T3.e[i].next)
	if(T3.e[i].type==0)ans[T3.e[i].id]=tr_sum(even,T3.e[i].y);
	else ans[T3.e[i].id]=tr_sum(odd,T3.e[i].y);
}
void dfs2(int x,int fa,bool tf)
{
	for(int i=T2.first[x];i;i=T2.e[i].next)//处理轻子树 
	if(T2.e[i].y!=fa&&T2.e[i].y!=mson[x])dfs2(T2.e[i].y,x,false);
	
	if(mson[x]!=0)dfs2(mson[x],x,true); add(a[x]);//处理重子树和自己 
	for(int i=T2.first[x];i;i=T2.e[i].next)//加上轻子树贡献 
	if(T2.e[i].y!=fa&&T2.e[i].y!=mson[x])go(T2.e[i].y,x,true);
	getans(x);//求解自己 
	
	if(!tf)go(x,fa,false);//如果自己是父亲的轻儿子,那么删去自己的贡献 
}

int main()
{
	scanf("%d %d",&n,&m); nn=n;
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	for(int i=1,x,y;i<=m;i++)
	{
		scanf("%d %d",&x,&y);
		if(!Map[par(x,y)])T1.buildroad(x,y),T1.buildroad(y,x),Map[par(x,y)]=true;
	}
	scanf("%d",&q);
	for(int i=1,type,x,y;i<=q;i++)
	{
		scanf("%d %d %d",&type,&x,&y);
		T3.buildroad(x,y,type,i);
	}
	tarjan(1);//圆方树板子 
	dfs1(1,0); dfs2(1,0,true);//dsu on tree 
	for(int i=1;i<=q;i++)printf("%d\n",ans[i]);
}
基于模拟退火的计算器 在线运行 访问run.bcjh.xyz。 先展示下效果 https://pan.quark.cn/s/cc95c98c3760 参见此仓库。 使用方法(本地安装包) 前往Releases · hjenryin/BCJH-Metropolis下载最新 ,解压后输入游戏内校验码即可使用。 配置厨具 已在2.0.0弃用。 直接使用白菜菊花代码,保留高级厨具,新手池厨具可变。 更改迭代次数 如有需要,可以更改 中39行的数字来设置迭代次数。 本地编译 如果在windows平台,需要使用MSBuild编译,并将 改为ANSI编码。 如有条件,强烈建议这种本地运行(运行可加速、可多次重复)。 在 下运行 ,是游戏中的白菜菊花校验码。 编译、运行: - 在根目录新建 文件夹并 至build - - 使用 (linux) 或 (windows) 运行。 最后在命令行就可以得到输出结果了! (注意顺序)(得到厨师-技法,表示对应新手池厨具) 注:linux下不支持多任务选择 云端编译已在2.0.0弃用。 局限性 已知的问题: - 无法得到最优解! 只能得到一个比较好的解,有助于开阔思路。 - 无法选择菜品数量(默认拉满)。 可能有一定门槛。 (这可能有助于防止这类辅助工具的滥用导致分数膨胀? )(你问我为什么不用其他语言写? python一个晚上就写好了,结果因为有涉及json读写很多类型没法推断,jit用不了,算这个太慢了,所以就用c++写了) 工作原理 采用两层模拟退火来最大化总能量。 第一层为三个厨师,其能量用第二层模拟退火来估计。 也就是说,这套方法理论上也能算厨神(只要能够在非常快的时间内,算出一个厨神面板的得分),但是加上厨神的食材限制工作量有点大……以后再说吧。 (...
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值