【JZOJ6011】天天爱跑步

description

长跑的目的不是更快,而是更强。 ——zjp’s blog
zjp最近迷上了长跑。为了防止被zjp强锋吹拂,小狗们决定躲到狗窝里去,现在已知有n条狗在一个二维平面直角坐标系的第一象限内。
狗是一种特殊的生物,每只在(x, y)的狗走一步只能到达(x + y, y),(x, y +x),(x − y, y),(x, y − x)这四个位置中的任意一个。并且任何时候,狗都不能在坐标轴上或在到达其它象限内的位置。
每个狗窝只能容纳一条狗,我们知道n个狗窝的坐标(也在第一象限内),每条狗不一定要到其对应编号的狗窝。
经过狗精密的计算发现,当所有狗到达狗窝的步数和最小时,狗是最安全的,尽管有的狗可能要走较多的步数。
现在,你只需要告诉他们:所有狗都到达狗窝的最小步数和。


analysis

  • 虚树+++哈希,这题有点神仙……但不难打

  • 先定义平面上的点向外移动指横或纵坐标变大,向里移动指横或纵坐标变小,而且容易知道移动可逆

  • 思考一下,(x,y)(x,y)(x,y)可以向外移动到(x+y,y),(x,y+x)(x+y,y),(x,y+x)(x+y,y),(x,y+x),但只能向里移动到两种中的一个(因为x−y≤0x-y≤0xy0y−x≤0y-x≤0yx0

  • 那么任意一个点都是向外扩展出两个点且向里扩展出一个点,把它们全部连起来,其实就是二叉树

  • 把所有点的横纵坐标除掉所有横纵坐标的gcdgcdgcd,此时所有的点都可从(1,1)(1,1)(1,1)移动得到(数据保证合法)

  • 因为移动是可逆的,所以狗窝可以视作和狗一样可以在二叉树上移动,那么把狗窝也算可移动的点

  • 观察(x−y,y),(x,y−x)(x-y,y),(x,y-x)(xy,y),(x,yx)并手动模拟点向里移动的过程,发现其实就是(x,y)(x,y)(x,y)两数求gcdgcdgcd的过程

  • x>yx>yx>y,具体就是,xxx变成xmodyxmodyxmodyyyy再变成ymodxymodxymodx,这样下去

  • 这样变化,一定是xxx先比yyy大,再yyyxxx大下去,在二叉树上的变化就是扭来扭去,这个会变化log⁡\loglog

  • 我们用哈希只把每个点和中间的转折点给建出来,边权即为x+yx+yx+y,这样建出一棵虚树,根为(1,1)(1,1)(1,1)

  • 把狗权值视作111,狗窝权值视作−1-11,那么类似树上差分,让111−1-11配对,同一子树里尽量配对最优

  • 这个一步步向上配对就可以,回溯时记录该子树内还有几个狗或窝没配对即可


code

#pragma GCC optimize("O3")
#pragma G++ optimize("O3")
#include<stdio.h>
#include<string.h>
#include<algorithm>
#include<vector>
#define mod 3000007
#define MAXN 3000005
#define ll long long
#define reg register ll
#define fo(i,a,b) for (reg i=a;i<=b;++i)
#define fd(i,a,b) for (reg i=a;i>=b;--i)
#define rep(i,a,b) for (reg i=last[a][b];i;i=next[i][b])
#define O3 __attribute__((optimize("-O3")))

using namespace std;

vector<ll>f[MAXN][2];
ll tr[MAXN],depth[MAXN];
ll n,m,bz,tot,ans,total;

struct node
{
	ll x,y;
}a[MAXN];

struct point
{
	ll x,y;
}hash[mod+5];

O3 inline ll read()
{
	ll x=0,f=1;char ch=getchar();
	while (ch<'0' || '9'<ch){if (ch=='-')f=-1;ch=getchar();}
	while ('0'<=ch && ch<='9')x=x*10+ch-'0',ch=getchar();
	return x*f;
}
O3 inline ll add(ll x,ll y)
{
	ll now=((x%mod)*258280327+(y%mod)+1000000007)%mod;
	while (hash[now].x && (hash[now].x!=x || hash[now].y!=y))(now+=1)%=mod;
	if (hash[now].x==0)hash[now].x=x,hash[now].y=y,bz=0,depth[now]=x+y;
	return now;
}
O3 inline void link_tree(ll x,ll y)
{
	bz=1;
	ll now=add(x,y),las,temp;
	if (bz)return;
	while (x!=y)
	{
 		las=add(x,y);
		if (x>y)x%=y,temp=0;
		else y%=x,temp=1;
		if (x==0 || y==0)x=y=1;
		bz=1,now=add(x,y);
		f[now][temp].push_back(las);
		if (bz)return;
	}
}
O3 inline ll gcd(ll x,ll y)
{
	return !(x%y)?y:gcd(y,x%y);
}
O3 inline bool cmp(ll x,ll y)
{
	return depth[x]>depth[y];
}
O3 inline void dfs(ll x)
{
	ll cnt=0;
	fo(j,0,1)
	{
		if (!f[x][j].size())continue;
		fo(i,0,f[x][j].size()-1)dfs(f[x][j][i]);
		sort(f[x][j].begin(),f[x][j].end(),cmp);
		ll las=f[x][j][0],val=tr[las],bz=j?hash[x].x:hash[x].y;
		fo(i,1,f[x][j].size()-1)
		{
			ans+=abs(val)*((depth[las]-depth[f[x][j][i]])/bz);
			val+=tr[f[x][j][i]],las=f[x][j][i];
		}
		cnt+=val,ans+=abs(val)*((depth[las]-depth[x])/bz);
	}
	tr[x]+=cnt;
}
O3 int main()
{
	freopen("a.in","r",stdin);
	freopen("a.out","w",stdout);
	n=read();
	fo(i,1,n<<1)tot=gcd(tot,gcd(a[i].x=read(),a[i].y=read()));
	fo(i,1,n<<1)a[i].x/=tot,a[i].y/=tot,link_tree(a[i].x,a[i].y);
	fo(i,1,n)++tr[add(a[i].x,a[i].y)];
	fo(i,n+1,n<<1)--tr[add(a[i].x,a[i].y)];
	dfs(add(1,1));
	printf("%lld\n",ans);
	return 0;
}
### 关于 '天天跑步' 的 C++ 编程竞赛题目分析 #### 题目背景与描述 在 C++ 编程竞赛中,“天天跑步”是一道经典的图论问题,通常涉及到路径查询和差分数组的应用。这类问题的核心在于如何高效处理大规模的数据输入并优化时间复杂度。根据以往的经验,在解决此类问题时,选手需要具备扎实的图论基础以及对数据结构的良好掌握能力[^1]。 #### 数据结构的选择 为了应对该类问题中的大量节点和边操作需求,推荐使用邻接表来存储图的信息。通过邻接表的方式可以显著减少空间占用,并提高访问效率。此外,还需要引入树上差分的思想来进行区间更新操作,从而实现快速求解目标路径上的特定属性值变化情况[^3]。 #### 实现方法概述 以下是基于上述理论框架的一个可能解决方案: ```cpp #include <bits/stdc++.h> using namespace std; const int MAXN = 1e5 + 5; vector<int> adj[MAXN]; int depth[MAXN], parent_node[MAXN][20]; // For LCA preprocessing long long diff_array[MAXN]; // Function to preprocess the tree for LCA queries using binary lifting technique. void dfs(int u, int p){ parent_node[u][0]=p; for(auto &v : adj[u]){ if(v != p){ depth[v]=depth[u]+1; dfs(v,u); } } } // Precompute all ancestor nodes up to log(n). void pre_process_lca(int n){ memset(parent_node,-1,sizeof(parent_node)); for(int j=1;(1<<j)<n;j++) { for(int i=1;i<=n;i++) { if(parent_node[i][j-1]!=-1) parent_node[i][j]=parent_node[parent_node[i][j-1]][j-1]; } } } // Query function that finds lowest common ancestor between two given vertices. int find_LCA(int a,int b){ if(depth[a]<depth[b]) swap(a,b); // Bring both elements at same level by moving higher one down appropriately via ancestors table entries indexed logarithmically. for(int k=19;k>=0 && (a!=b);k--){ if((depth[a]-pow(2,k)) >= depth[b]) a=parent_node[a][k]; } if(a==b)return a; // Now move them together until their parents become equal which will be our answer then. for(int k=19;k>=0;k--){ if(parent_node[a][k]!=-1&&parent_node[a][k]!=parent_node[b][k]){ a=parent_node[a][k]; b=parent_node[b][k]; } } return parent_node[a][0]; } // Update difference array based on path from node A -> B including endpoints themselves too ! void update_path_diffs(int start_point , int end_point ){ while(start_point !=end_point ){ diff_array[start_point]++; start_point=find_parent_of_current_level_zero_or_above_itself_if_already_at_root(start_point ); } } int main(){ ios::sync_with_stdio(false); cin.tie(NULL); int N,Q; cin>>N>>Q; for(int i=1;i<N;i++){ int x,y; cin>>x>>y; adj[x].push_back(y); adj[y].push_back(x); } dfs(1,0); pre_process_lca(N); vector<pair<int,pair<int,char>>> events(Q+1,{}); string s; getline(cin,s); for(int q=1;q<=Q;q++){ char type; int person_id,distance_run_today; cin>>type>>person_id>>distance_run_today; if(type=='R'){ int current_location,last_seen_position; cin>>current_location>>last_seen_position; // Perform necessary updates here according to problem statement requirements... update_path_diffs(current_location,last_seen_position); }else{ cout<<(events[q].second.first ? "YES\n":"NO\n"); } } } ``` 此代码片段展示了如何利用深度优先搜索(DFS)、最近公共祖先(LCA)预处理技术以及路径差分技巧共同作用以解答“天天跑步”的核心部分——即计算某个人跑过的路程覆盖了多少个指定位置点的问题。 #### 总结 针对 “天天跑步” 类型的题目,重点在于理解其背后的数学模型转换过程,即将实际场景抽象成为易于计算机处理的形式;同时熟练运用各种高级数据结构如树状数组、线段树或者本案例提到的树形差分等工具完成最终的目标达成。持续不断地练习类似难度级别的习题有助于加深对该领域知识点的理解程度[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值