原题单连接:【图论2-1】基础树上问题
这两天写了一下洛谷基础树上题单,小小的做一个总结,不然又忘了,收获还是有的,还有两题没写,一题是原来在牛客看题解做过了,还有一题(P5666 树的重心)感觉有点难,后面再补,感觉自己做的题型还是太少了,慢慢来吧。
P5836 [USACO19DEC]Milk Visits S
这道题应该是最简单的一题吧,把两种牛所在的节点赋予0或者1权值,统计路径和然后与询问比较就行,这里我把G权值赋值为1;
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e9;
const int N=1e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op;
int dep[N],d[N][22],nxt[N*2],head[N*2],to[N*2],sum[N];
char s[N],c[2],ans[N];
void add(int x,int y){nxt[++T]=head[x];head[x]=T;to[T]=y;}
void dfs(int now,int fa,int dp) {
dep[now]=dp;
d[now][0]=fa;
sum[now]=sum[fa]+(s[now]=='G');//因为这个布尔表达式找了很久的bug
for(int i=1;(1<<i)<=dp;i++) {
d[now][i]=d[d[now][i-1]][i-1];
}
for(int i=head[now];i;i=nxt[i]) {
int u=to[i];
if(u==fa)continue;
dfs(u,now,dp+1);
}
}
int LCA(int x,int y) {
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--) {
if(dep[d[x][i]]>=dep[y]) {
x=d[x][i];
}
}
if(x==y)return x;
for(int i=20;i>=0;i--) {
if(d[x][i]!=d[y][i]) {
x=d[x][i];
y=d[y][i];
}
}
return d[x][0];
}
pair<int,int> getpair(int x,int y){//返回祖先和路径和
int z=LCA(x,y);
return mp(sum[x]+sum[y]-2*sum[z]+(s[z]=='G'),dep[x]+dep[y]-2*dep[z]+1);
}
void solve(){
scanf("%d%d",&n,&m);
scanf("%s",s+1);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs(1,1,1);
while(m--){
scanf("%d%d%s",&x,&y,c);
pair<int,int> e=getpair(x,y);
if((c[0]=='G'&&e.fi==0)||(c[0]=='H'&&e.fi==e.se))ans[k++]='0';//如果路径和为0但是你需要1或者你需要0但是路径和等于路径节点个数(即没有0)都是不合法的,其它是合法的
else ans[k++]='1';
}
for(int i=0;i<k;i++)printf("%c",ans[i]);
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题感觉还是很好的,树的直径的两种求法在这道题中完美的应用出来了,由于没有过多的了解树的直径的一些性质,这道题理解了好久。
对于k=1的时候很好考虑,对于一个树,如果你添加了一条边,肯定产生了一个环,那么这个环省下来的路径就是直径的大小乘上2减1,那么如果想要环最大,那么肯定连接树直径上的两个点环长最长,所以k=1的时候求一下直径输出直径长*2-1即可(减一是因为加的那条边也要走)。
对于k=2的时候,我们肯定还是要考虑直径,第一条边还是要连接直径,关键是第二个直径带来的贡献怎么求,第二条直径分成两种情况讨论,第一种是与第一条没有公共边,那么按照第一种情况一样计算就行;第二种情况是有公共边,那么对于在第一条直径中公共边的部分收益相当于没有了,也就是公共边还要走两次,那么怎么把这种想法给体现出来呢,那就是修改边权,将第一条直径上的点的边权全部改成-1(相当于x+y-y=x,y代表公共部分,取反后他的贡献就没有了),所以这道题就是求两次直径,两次直径的求法也不同,第一条由于需要知道路径所以需要dfs求,第二条由于有负边,所以需要树形dp来求。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e9;
const int N=1e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,s,mx,ok;
int dp[N],path[N],tot,ans,vis[N];
vector<pair<int,int> >v[N];
void dfs1(int now,int fa,int cnt){//双dfs求直径
if(cnt>tot){
tot=cnt;
s=now;
}
for(auto e:v[now]){
if(e.fi==fa)continue;
dfs1(e.fi,now,cnt+1);
}
}
void dfs2(int now,int fa,int cnt){
if(cnt>tot){
tot=cnt;
t=now;
}
for(auto e:v[now]){
if(e.fi==fa)continue;
dfs2(e.fi,now,cnt+1);
}
}
void dfs3(int now,int fa,int cnt){//这一步求路径其实可以合并到dfs2中
if(ok)return;
if(cnt==tot){
vis[s]=1;
for(int i=1;i<=cnt;i++)vis[path[i]]=1;//把路径上的点标记
ok=1;
return;
}
for(auto e:v[now]){
if(e.fi==fa)continue;
path[++T]=e.fi;
dfs3(e.fi,now,cnt+1);
--T;
}
}
void dfs4(int now,int fa){//树形dp求直径大小
for(auto e:v[now]){
if(e.fi==fa)continue;
dfs4(e.fi,now);
ans=max(ans,dp[now]+dp[e.fi]+e.se);
dp[now]=max(dp[now],dp[e.fi]+e.se);
}
}
void solve(){
scanf("%d%d",&n,&k);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
v[x].pb(mp(y,1));v[y].pb(mp(x,1));
}
dfs1(1,1,0);
dfs2(s,s,0);
dfs3(s,s,0);
if(k==1){printf("%d\n",2*(n-1)-tot+1);return;}
for(int i=1;i<=n;i++){
if(!vis[i])continue;
for(int j=0;j<le(v[i]);j++){
if(vis[v[i][j].fi])v[i][j].se=-1;//如果两个点都已经被标记了,那么这条边就是第一条直径上的边
}
}
dfs4(1,1);
printf("%d\n",2*(n-1)-ans-tot+2);
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题由于之前做过,所以感觉还是蛮简单的。只不过理解的更深了吧。
假设我们先以1为根求完所有的节点的深度和(深度和就代表所有节点到根1位置的路径和ans),那么当根节点(假如为u)改变时(假如更换为v,其中u是v的父亲节点)对于答案的贡献分为两种情况讨论,第一种是以v为节点的子树大小,这个地方每一个节点路径值都要减1,因为他们不用到u了,此外所有的(n-siz[v])个节点都要加上1,因为他们要到达v了,所以以v为根的总路径和就是ans+n-siz[v]-siz[v];然后在这些和中取最小值就行。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e15;
const int N=1e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,id=1;
int dep[N],siz[N];
LL ans;
vector<int>v[N];
void dfs(int now,int fa,int cnt){//先统计深度和子树大小
dep[now]=cnt;
siz[now]=1;
for(auto e:v[now]){
if(e==fa)continue;
dfs(e,now,cnt+1);
siz[now]+=siz[e];
}
}
void findans(int now,int fa,LL res){//dfs搜索出以每一个节点为根时的最优值
if(ans>=res){
if(ans>res||now<=id)id=now;
ans=res;
}
for(auto e:v[now]){
if(e==fa)continue;
findans(e,now,n-2*siz[e]+res);
}
}
void solve(){
scanf("%d",&n);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
v[x].pb(y);v[y].pb(x);
}
dfs(1,1,0);
for(int i=1;i<=n;i++)ans+=dep[i];//以1为根的路径和
findans(1,1,ans);
printf("%d %lld\n",id,ans);
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题就是LCA的应用吧,想到了就简单,求两个路径有没有相交点,可以看出若有相交点,那么要么是第一条路径上的LCA在另一条上,或者反过来,当然也可以都满足,那么怎么判断某一个点是否在一条路径上呢,那就是利用路径和(两点之间的距离等于两点到中间某一个点的距离之和)。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e15;
const int N=1e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,s;
int dep[N],d[N][22],nxt[N*2],head[N*2],to[N*2];
void add(int x,int y) {
nxt[++T]=head[x];
head[x]=T;
to[T]=y;
}
void dfs(int now,int fa,int dp) {
dep[now]=dp;
d[now][0]=fa;
for(int i=1;(1<<i)<=dp;i++) {
d[now][i]=d[d[now][i-1]][i-1];
}
for(int i=head[now];i;i=nxt[i]) {
int u=to[i];
if(u==fa)continue;
dfs(u,now,dp+1);
}
}
int LCA(int x,int y) {
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--) {
if(dep[d[x][i]]>=dep[y]) {
x=d[x][i];
}
}
if(x==y)return x;
for(int i=20;i>=0;i--) {
if(d[x][i]!=d[y][i]) {
x=d[x][i];
y=d[y][i];
}
}
return d[x][0];
}
int dis(int x,int y){//获取路径
return dep[x]+dep[y]-2*dep[LCA(x,y)];
}
void solve(){
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs(1,1,1);
while(m--){
scanf("%d%d%d%d",&x,&y,&s,&t);
int e1=LCA(x,y);
int e2=LCA(s,t);
if(dis(e1,s)+dis(e1,t)==dis(s,t)||dis(e2,x)+dis(e2,y)==dis(x,y))puts("Y");//判断LCA是否在另一条路径上
else puts("N");
}
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题也是和LCA联系很大了,首先我们需要理解一点,若只有两个点a,b,那么我们就只需要求两个点的LCA(a,b)就行,两点之间线段最短,那还有一个点c就是求该点到这条线段的最短距离是什么,可以看出要么选择LCA(a,c),或者选择LCA(b,c);而且我们通过模拟发现,这三个点两两之间的LCA会有两个是相同的,并且选择的那个聚集点一定是三个LCA深度最小的那个点,那么接下来就是求三个点到聚集点的距离问题了。疑惑的是倍增LCA竟然比树剖LCA慢一点,可能我倍增写的水了吧。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e15;
const int N=5e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,z,xx,yy,zz;
int dep[N],nxt[N*2],head[N*2],to[N*2],f[N],siz[N],top[N],son[N],id[N];
void add(int x,int y) {
nxt[++T]=head[x];
head[x]=T;
to[T]=y;
}
void dfs1(int now,int fa){
f[now]=fa;
siz[now]=1;
dep[now]=dep[fa]+1;
int mx=0;
for(int i=head[now];i;i=nxt[i]) {
int e=to[i];
if(e==fa)continue;
dfs1(e,now);
siz[now]+=siz[e];
if(siz[e]>siz[mx])mx=e;
}
if(mx)son[now]=mx;
}
void dfs2(int now,int fa){
id[now]=++T;
top[now]=fa;
if(!son[now])return;
dfs2(son[now],fa);
for(int i=head[now];i;i=nxt[i]) {
int e=to[i];
if(e==fa||e==son[now]||id[e])continue;
dfs2(e,e);
}
}
int LCA(int x,int y) {//树剖LCA
for(;top[x]!=top[y];dep[top[x]]>dep[top[y]]?x=f[top[x]]:y=f[top[y]]);
return dep[x]<dep[y]?x:y;
}
int dis(int x,int y){
int e=LCA(x,y);
return dep[x]+dep[y]-2*dep[e];
}
void solve(){
scanf("%d%d",&n,&m);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs1(1,1);
dfs2(1,1);
while(m--){
scanf("%d%d%d",&x,&y,&z);
xx=LCA(x,y);yy=LCA(x,z);zz=LCA(y,z);
if(dep[xx]<dep[yy])swap(xx,yy);
if(dep[xx]<dep[zz])swap(xx,zz);
if(dep[yy]<dep[zz])swap(yy,zz);
printf("%d %d\n",xx,dis(xx,x)+dis(xx,y)+dis(xx,z));//或者printf("%d %d\n",xx,dep[x]+dep[y]+dep[z]-dep[xx]-2*dep[zz]);
}
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题最开始看错题想了好久,看成类似子图的问题了,结果最后发现是路径,这道题也是分类讨论吧,不过我写的可能不是正解,跑的挺慢的。
首先最简单的就是没有这个颜色,那么答案就是n*(n-1)/2,其次是颜色个数只有一个的,这个也很好想,就是这个节点的子树大小乘上非子树的其它节点个数,此外不同子树之间还可以构成点对,也就是每一个子树之间两两相乘的方案数,这个计数暴力O(n^2),可以利用前缀和变成O(n),也可以利用数学表达式变成O(n)。
其次就是两个或者是以上的节点颜色个数了;这个分为三种情况讨论,
首先所有该颜色节点都在一条链上,这个怎么判定呢,我们取链上深度最大的节点与其它节点求LCA,那么LCA一定是与它进行LCA的节点。
其次,若不在一条链上,那么最多在两条链上(三条链就不存在点对包含所有相同颜色节点了,这个也是第三种情况)。这种情况对于答案的贡献与一条链上的情况一样,都是两个端点带来的贡献,第一种很好判断一定是深度最深和最浅的那两个。对于两条链的情况,首先我们先把节点按照深度进行排序,那么深度最大的一定是其中的一个端点,所以我们就拿这个端点x与其他相同颜色的节点y做LCA,那么如果LCA为y,那么y就在x的这条链上,反之y就在另一条链上,且y是另一条链上深度最大的节点,记录此时节点为y;接下来就是判断是否是两条链,我们分析可知若只有两条链,那么其它节点与x,y的LCA只有两种情况,要么等于自身(在x或者y这条链上),要么等于LCA(x,y),所以我们可以以此为判断条件判断是否是两条链。
接下来就是统计两个端点带来的贡献了,也很好想,一条链上的答案就是深度较大的节点子树大小乘上除去另一个端点的子树大小,另一个端点的子树大小可以用在这条链上的端点的儿子子树大小求得(这个地方当然可以倍增求了)。两条链就更好求了,就是两个端点的子树大小相乘。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e15;
const int N=1e6+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,z,xx,yy,zz;
int dep[N],d[N][22],nxt[N*2],head[N*2],to[N*2],siz[N],f[N];
vector<int>v[N];
struct node{
int id,dp;
bool operator<(const node &p)const{
return dp>p.dp;
}
};
void add(int x,int y){nxt[++T]=head[x];head[x]=T;to[T]=y;}
void dfs(int now,int fa,int dp) {
dep[now]=dp;
d[now][0]=fa;
siz[now]=1;
f[now]=fa;
for(int i=1;(1<<i)<=dp;i++) {
d[now][i]=d[d[now][i-1]][i-1];
}
for(int i=head[now];i;i=nxt[i]) {
int u=to[i];
if(u==fa)continue;
dfs(u,now,dp+1);
siz[now]+=siz[u];
}
}
int LCA(int x,int y) {
if(dep[x]<dep[y])swap(x,y);
for(int i=20;i>=0;i--) {
if(dep[d[x][i]]>=dep[y]) {
x=d[x][i];
}
}
if(x==y)return x;
for(int i=20;i>=0;i--) {
if(d[x][i]!=d[y][i]) {
x=d[x][i];
y=d[y][i];
}
}
return d[x][0];
}
LL cal(int now,int fa){//统计一个节点带来的贡献
LL res=0,x=1;
for(int i=head[now];i;i=nxt[i]){
int u=to[i];
if(u==fa)continue;
res+=x*siz[u];
x+=siz[u];//前缀和累乘
}
return res+1LL*(n-siz[now])*siz[now];//加上该点两端带来的贡献
}
LL fans(int now){//两个节点以上的答案求法
vector<node>q;
for(auto e:v[now])q.pb(node{e,dep[e]});
sort(all(q));//按照深度进行排序
int x=q[0].id,y=-1;//取最深的节点作为一个端点,找另外一个
bool ok=true;//先判断是否是一条链
for(auto e:v[now]){
if(e==x)continue;
t=LCA(e,x);
if(t==e)continue;
ok=false;
y=e;//LCA不同说明是另一条链的端点
break;
}
if(ok){//是一条链直接统计答案
t=le(q);
y=q[t-1].id;
int te=x;
for(int i=20;i>=0;i--)if(dep[d[te][i]]>dep[y])te=d[te][i];//倍增找y的儿子节点,且儿子节点在这条链上
return 1LL*siz[x]*(n-siz[te]);
}
k=LCA(x,y);
for(auto e:q){
if(e.id==y||e.id==x)continue;
int c=e.id;
int a=LCA(c,x),b=LCA(c,y);
if(a==c&&b==k||b==c&&a==k)continue;//判断是否满足两条链的条件
return 0;
}
return 1LL*siz[x]*siz[y];
}
void solve(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&x);
v[x].pb(i);
}
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
add(x,y);add(y,x);
}
dfs(1,1,1);
for(int i=1;i<=n;i++){
if(le(v[i])==0)printf("%lld\n",1LL*n*(n-1)/2);
else if(le(v[i])==1){
printf("%lld\n",cal(v[i][0],f[v[i][0]]));
}else {
printf("%lld\n",fans(i));
}
}
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}
这道题也是树直径的一种利用吧,首先若只有一个点那么这个点肯定选在直径的中点,所以我们需要双dfs求得直径和直径上的路径,所以这个时候我们需要换根为中点重新建立这棵树,那么其它的k-1个城市的选择就在中点的子树种进行选择。然后我们可以每一次放置的时候都肯定选择某一条链上深度差最大的,深度差最大肯定到根的距离最大嘛。然后我们就可以对于每一个节点统计他的子树中节点最大的深度与该节点的深度差最大是多少,然后按照这个差排序,那么排前k个的节点就是需要作为中心的节点。那么放完之后的答案就是第k+1个节点的差+1。
代码如下:
#include<bits/stdc++.h>
#define all(x) (x).begin(),(x).end()
#define le(x) ((int)(x).size())
#define LL long long
#define mp make_pair
#define pb push_back
#define fi first
#define se second
#define db printf("Here!\n");
using namespace std;
const double eps=1e-6;
const LL inf=1e15;
const int N=1e5+5;
const int M=2e6+5;
const int mod=998244353;
int n,m,k,t,T,len,x,y,op,s,mid;
int dep[N],path[N],cha[N];
vector<int>v[N];
void dfs1(int now,int fa,int cnt){
if(cnt>len){
len=cnt;
s=now;
}
for(auto e:v[now]){
if(e==fa)continue;
dfs1(e,now,cnt+1);
}
}
void dfs2(int now,int fa,int cnt){//求直径和路径中点
path[cnt]=now;
if(cnt>len){
mid=path[(cnt+1)/2];
len=cnt;
t=now;
}
for(auto e:v[now]){
if(e==fa)continue;
dfs2(e,now,cnt+1);
}
}
int dfs4(int now,int fa){//以中点为根建立树,并统计深度差
dep[now]=dep[fa]+1;
int mx=dep[now];
for(auto e:v[now]){
if(e==fa)continue;
mx=max(dfs4(e,now),mx);
}
cha[now]=mx-dep[now];
return mx;
}
void solve(){
scanf("%d%d",&n,&k);
for(int i=1;i<n;i++){
scanf("%d%d",&x,&y);
v[x].pb(y);v[y].pb(x);
}
dfs1(1,1,1);
len=0;
dfs2(s,s,1);
dfs4(mid,mid);//重新建立这颗树
sort(cha+1,cha+1+n,greater<int>());
printf("%d\n",cha[k+1]+1);
}
int main(){
//int o;scanf("%d",&o);
//while(o--){
solve();
//}
return 0;
}