ZJOI2018]历史 做题心得
[ZJOI2018]历史 做题心得
whk了好久,来点lct练练手,免得手生了
是一个思维好题,巧妙运用了 LCT 的 access
做法概要
dp一遍求出初始的答案
用 LCT 维护修改
不考虑修改,求初始答案
考虑每条边的贡献加起来。
发现边的选择并没有后效性,只需要分别保证每条边改的次数最多加起来就行了。
所以可以像树形 dp 一样解决不带修改的问题。
为什么没有后效性:
设 ,现在选到了 这条边。
对于 子树 中的每个点 , 城市的崛起,除非是第一次,都会让 边的势力改变,并可能对答案产生贡献。
(如果这次崛起的城市和上次崛起的城市,位于 的同一颗子树内,那么就会在 下面贡献,不会到 这边贡献 )
注意到灾难度算的是 不同 的势力数量。每次有新城市崛起的时候,在 边已经算过了一次贡献,那么在 边就没有贡献了。
所以说 子树中的选择只会对 产生影响,一定不会影响
那么考虑一下 这条边要被搞多少次。
上面已经提到,必须是在 的 不同 子树里选,或者直接选择 本身,才会产生贡献。
↑ 如图,必须在不同的子树里两个城市先后崛起 (如图中的红色点和绿色点),才会让 边贡献。
相反,如果在同一颗子树里,比如说两个红色子树的城市,先后崛起
那么它的贡献就会贡献到 下面的红色边,而不是 边
考虑计算最大能贡献多少次。设 表示 子树中 的和。 ( 就是崛起多少次那个 )
设 有 个儿子,为 。
那相当于有 种颜色的球,每种颜色的球分别有: 个
问题转化成,把这些球排成一排,最大化相邻的球颜色不同的对数。设 为总个数, 为最大的个数,并且这个数量最大的颜色是 。
即 ,, 表示 取的是哪一项
如果 ,那么最优方案显然就是,每次先放除了 的一个颜色,然后放一个 。这样能做到不同的数量是 。
否则,可以一个一个轮流的放,做到相邻的都不同。答案是 。
然后就可以求出初始的答案了。
分析性质,维护修改
,即 。
考虑一个 ,它 最大的儿子是 。
定义:如果 ,那么称 是 的 重儿子。类似的, 为 重边,重边组成的链是 重链。除了重边之外的边称为 轻边。
轻边的数量是多少呢?轻边满足:,那每走一条轻边, 至少要 ,那这个复杂度就是 级别的。
注意这里的 。代码实现中也要注意。
如果您 WA 50,错的是后半部分,那可能就是这个问题
显然,一个 的重儿子一定不超过一个(注意,可能没有)
考虑一次修改,对重链的影响:答案是 , 的增加量都一样,消掉了,于是 是没有变化的。
换句话说,修改操作对于一条重链的答案没有任何影响。那它就只对轻边有影响。
于是得到,贡献答案只需要考虑轻边
那直接树链剖分跳就完了?那不太行,发现每次修改可能会改变一些重边的连接。
但是假设改的是 ,那么 到根的链只可能变重,不可能变轻 (因为是增加)。
于是得到,修改链接只需要考虑轻边
它可能会翻身成为重边, 也可能会让原来的重边变轻
综上,我们 只需要考虑轻边
注意到要改变连接,用 维护。由于只需要考虑轻边,于是只需要它一个 ,中途顺带着改一改,就解决了。
代码
#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
#define N 400005
#define int long long
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define all(a) a.begin(),a.end()
#define iter(a,p) (a.begin()+p)
#define PUT(a,n) F(i,1,n) printf("%d ",a[i]); puts("");
int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
template <typename T> void Rd(T& arg){arg=I();}
template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
int n,m,a[N];
class Graph
{
public:
struct edge{int v,nx;} e[N<<1];
int head[N]; int ecnt=-1;
void clear()
{
ecnt=-1; MEM(head,-1); MEM(e,-1);
}
void add(int u,int v)
{
e[++ecnt]=(edge){v,head[u]}; head[u]=ecnt;
} void add2(int u,int v) {add(u,v); add(v,u);}
int st(int u) {return head[u];}
int to(int i) {return e[i].v;}
int nx(int i) {return e[i].nx;}
}G;
void Input()
{
Rd(n,m); F(i,1,n) a[i]=I();
G.clear();
F(i,1,n-1)
{
int u,v; Rd(u,v);
G.add2(u,v);
}
}
// 定义重儿子: size[v]*2>size[u] 的 v 是 u 的重儿子
// LCT 维护重链
int ans=0;
class Link_Cut_Tree
{
public:
int fa[N],ch[N][2];
int val[N],other[N],s[N]; // other: 轻子树的和
int res[N];
#define ls(u) ch[u][0]
#define rs(u) ch[u][1]
#define sub(u) (s[u]-s[ls(u)])
// LCT 的辅助树上, 左子树表示祖先部分
// 所以减去祖先的部分就是实际的子树
bool id(int u) {return u==rs(fa[u]);}
bool rt(int u) {return u!=ls(fa[u]) and u!=rs(fa[u]);}
void up(int u)
{
s[u]=s[ls(u)]+s[rs(u)]+val[u]+other[u];
}
void upans(int u)
{
ans-=res[u];
res[u]=sub(u)-1;
if (rs(u)) res[u]=(sub(u)-s[rs(u)])*2;
if (val[u]*2>sub(u)) res[u]=(sub(u)-val[u])*2;
ans+=res[u];
}
void rot(int u)
{
int f=fa[u],f2=fa[f],i=id(u),i2=id(f),w=ch[u][i^1];
if (!rt(f)) ch[f2][i2]=u; ch[u][i^1]=f; ch[f][i]=w;
if (w) fa[w]=f; fa[f]=u; fa[u]=f2;
up(f); up(u);
}
void sp(int u)
{
while(!rt(u))
{
int f=fa[u];
if (!rt(f)) rot(id(u)==id(f)?f:u);
rot(u);
}
}
void ac(int u,int a)
{
for(int p=0;u;p=u,u=fa[u])
{
sp(u); s[u]+=a; u[p?other:val]+=a;
if (s[rs(u)]*2<=sub(u)) // 原来的重儿子不再重
{
other[u]+=s[rs(u)]; rs(u)=0;
}
if (s[p]*2>sub(u)) // 更新的儿子变重
{
other[u]-=s[p]; rs(u)=p;
}
upans(u);
}
}
}T;
void DFS(int u,int f)
{
T.val[u]=T.s[u]=a[u];
T.fa[u]=f;
Tra(i,u) if (v!=f)
{
DFS(v,u);
T.s[u]+=T.s[v];
}
int k=0;
Tra(i,u) if (v!=f)
{
if (T.s[v]*2>T.s[u]) k=v;
}
T.ch[u][1]=k; // 重儿子
T.other[u]=T.s[u]-a[u]-T.s[k];
T.upans(u);
}
void Sakuya()
{
DFS(1,0);
printf("%lld\n",ans);
F(i,1,m)
{
int x,y; Rd(x,y);
T.ac(x,y);
printf("%lld\n",ans);
}
}
void IsMyWife()
{
Input();
Sakuya();
}
}
#undef int //long long
int main()
{
Flandre_Scarlet::IsMyWife();
getchar();
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】