还是集训
Day 1-基础算法
一、贪心
大概一般是按某种顺序遍历,每次选择价值最大/对后影响最小/限制最大的作为答案
kle.区间选择模型
一般是根据区间的性质,按照左/右端点排序贪心选择;必要时省去删除包含关系的区间
可用调整法证明贪心的正确性:若合法,则一定存在一种最优解选择了它
klee.匹配模型
仍是要给前/后缀排序,然后贪心选择可选的最大/小的数;若是区间匹配或前/后缀带点权,则有序遍历右部点,选择可选的限制最大的点 ( 比如在区间中,限制最大的点就是长度最小的点 )。如果是二维,那就形似扫描线扫描,每次仍是选择限制最大的点
也可用调整法证明正确性
kleee.邻项交换模型
可以先假想出来一个不优的方案,然后交换相邻两项
e.g.给出若干 01 串,要求将这些 01 串有序拼接后逆序对的数量最少。可以有一个初步想法:0 多的放前面,反之放后面,但这不够准确。记
可通过反证法证明贪心正确性
1.1 树上01
最刚开始可以有个思路:能选 0 就选,不选再说,但这课上已有 hack 数据(子树内部)
先考虑一个节点连着若干已确定顺序的子树的模型。这样相当于每个子树就是一个 01 串,要求顺序最优——这和邻向交换模型一毛一样,记个
哎,然后就可以从一个大问题转为一堆子问题了。为了方便计算,在实现时将子树内部的信息传递至根(形似合并),然后每次合并计算这次合并会产生多少逆序对。于是乎用一个小根堆维护 显然 合并后的父节点肯定比原来的父节点先到堆顶,所以用一个
码
#include <bits/stdc++.h>
#define int long long
#define pdi pair<double,int>
#define mkp make_pair
using namespace std;
const int N=2e5+5;
const double inf=1e18;
int n;
int p[N],v[N];
int cnt0[N],cnt1[N];
int fa[N],vis[N],ans;
priority_queue < pdi,vector<pdi>,greater<pdi> > q;
int find_fa(int x)
{
if (fa[x]==x) return x;
return fa[x]=find_fa(fa[x]);
}
void _merge(int x,int y)
{
int fa1=find_fa(x),fa2=find_fa(y);
ans+=cnt1[fa1]*cnt0[fa2];
cnt0[fa1]+=cnt0[fa2],cnt1[fa1]+=cnt1[fa2];
fa[fa2]=fa1;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
for (int i=2;i<=n;i++) cin>>p[i];
for (int i=1;i<=n;i++)
{
cin>>v[i];
fa[i]=i;
if (v[i]) cnt1[i]++;
else cnt0[i]++;
q.push(mkp((cnt0[i]?(1.0*cnt1[i])/(1.0*cnt0[i]):inf),i));
}
while (!q.empty())
{
int ea=q.top().second;
q.pop();
if (vis[ea]||ea==1) continue;
vis[ea]=1;
int faa=find_fa(p[ea]);
_merge(faa,ea);
q.push(mkp((cnt0[faa]?(1.0*cnt1[faa])/(1.0*cnt0[faa]):inf),faa));
}
cout<<ans;
return 0;
}
1.2 带权前缀匹配
这是个带权前缀匹配,这个“前缀”是每个订单可以入住的房间,所以先给房间信息按容纳人数降序排序
然后每个订单的钱是固定的,想要收费最大就要维护费最少(即房间容量最小),所以二分查找一个可以入住的容量最小的房间,这就是相对于该订单最优的方案。
但要是负贡献,要它无用,所以不计
贪心地想,付款多的订单大概能贡献更多,所以把订单按照付款降序排序,然后计算就行
二分错了……不嘻嘻
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e5+5;
int n,m,o;
struct node { int val,num; }a[N],b[N];
int fa[N];
int ans[N],tol,sum;
bool cmp(int x,int y) { return x>y; }
bool cmp1(node x,node y) { return (x.num!=y.num?x.num>y.num:x.val>y.val); }
bool cmp2(node x,node y) { return (x.val!=y.val?x.val>y.val:x.num<y.num); }
int find_fa(int x) { return (fa[x]==x?x:fa[x]=find_fa(fa[x])); }
int BS(int x)
{
int l=1,r=n;
while (l<=r)
{
int mid=(l+r)>>1;
if (a[mid].num>=x) l=mid+1;
else r=mid-1;
}
return find_fa(r);
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m>>o;
for (int i=1;i<=n;i++) fa[i]=i;
for (int i=1;i<=n;i++) cin>>a[i].val>>a[i].num;
for (int i=1;i<=m;i++) cin>>b[i].val>>b[i].num;
sort(a+1,a+1+n,cmp1),sort(b+1,b+1+m,cmp2);
for (int i=1;i<=m;i++)
{
int ea=BS(b[i].num);
if (!ea||a[ea].val>=b[i].val) continue;
ans[++tol]=b[i].val-a[ea].val;
fa[ea]=find_fa(ea-1);
}
sort(ans+1,ans+tol+1,cmp),o=min(o,tol);
for (int i=1;i<=o;i++) sum+=ans[i];
cout<<sum;
return 0;
}
kleeee.凸性
其实没听
若对于
(直白讲,就是斜率越来越大)
在某些问题中,要是关注的是关于
凸函数卷积:给出两个凸函数
看着不好求,但可以利用凸函数的性质。凸函数的差值具有单调性,所以可以分别求出两个凸函数的差分数组,此时凸函数卷积就变成了
1.2
kleeeee.拟阵
听了没懂,所以待补
二、二分
二分答案往往是需要答案具有单调性,但有的题的二分并不需要单调性,这种往往是通过二分不断判断当前集合子集的性质并缩小范围,最后找到所求元素。神奇的嘞
2.1 Foo Fifhters
三、倍增
如果暴力是一步步,那么可以直接倍增大跨步
3.1
Day 2-动态规划
序列
1.1
首先显然,清空之后万事空。所以找出最后一次必须要清空的
斜率优化
听半天居然都听错了 悲
若
首先 显然 并不是所有的转移点
斜率优化DP
设状态
为了转移方便,开局给
那么可以将其斜率看作为
容易发现 在这道题中的斜率和横坐标都是单调不降的,那么直接用单调队列维护就好
板?
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
int n,L;
int sum[N],f[N];
int q[N],hd,tl;
int x(int i) { return sum[i]; }
int y(int i) { return f[i]+sum[i]*sum[i]; }
double kk(int i,int j) { return 1.0*(y(j)-y(i))/(1.0*x(j)-x(i)); }
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>L,L++;
for (int i=1;i<=n;i++) { cin>>sum[i]; sum[i]+=sum[i-1]+1; }
for (int i=1;i<=n;i++)
{
while (hd<tl&&kk(q[hd],q[hd+1])<=(sum[i]-L)*2) hd++;//根据斜率找到j
f[i]=f[q[hd]]+(sum[i]-sum[q[hd]]-L)*(sum[i]-sum[q[hd]]-L);
while (hd<tl&&kk(q[tl-1],q[tl])>=kk(q[tl-1],i)) tl--;//一定要这么写…
q[++tl]=i;
}
cout<<f[n];
return 0;
}
决策单调性优化
若
决策单调性优化DP
设状态
经过打表后 发现决策最优点具有单调性
但这里不能直接用双指针,因为不保证它不是曲里拐弯的
什么是决策单调性呢。可以画出一个矩阵,
豪德,每当更新了一个
道理我都懂关键是程序实现啊 用一个单调队列维护那些个最优转移点,再记
码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e5+5;
int T;
int n,l,p;
int sum[N],pre[N];
long double f[N];
int k[N],q[N],hd,tl;
string str[N];
inline long double qsm(long double x,int y)
{
long double res=1;
if (x<0) x=-x;
while (y)
{
if (y&1) res*=x;
x*=x,y>>=1;
}
return res;
}
inline long double calc(int j,int i) { return f[j]+qsm(sum[i]-sum[j]-l,p); }
inline int BS(int x,int y)
{
int l=x,r=n+1;
while (l<r)
{
int mid=(l+r)>>1;
if (calc(y,mid)<=calc(x,mid)) r=mid;
else l=mid+1;
}
return r;
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>T;
while (T--)
{
cin>>n>>l>>p,l++;
for (int i=1;i<=n;i++) { cin>>str[i]; sum[i]=sum[i-1]+str[i].size()+1; }
hd=tl=1,q[1]=0;
for (int i=1;i<=n;i++)
{
while (hd<tl&&k[hd]<=i) hd++;//将不覆盖i的前缀pop掉
f[i]=calc(q[hd],i),pre[i]=q[hd];
while (hd<tl&&k[tl-1]>=BS(q[tl],i)) tl--;//将被完全覆盖的转移点 pop 掉
k[tl]=BS(q[tl],i),q[++tl]=i;
}
if (f[n]>1e18) cout<<"Too hard too arrange\n";
else//史一般的输出
{
cout<<(int)(f[n]+0.5)<<"\n";
tl=0,q[0]=n;
for (int i=n;i;i=pre[i]) q[++tl]=pre[i];
int idx=1;
for (int i=tl;i;i--)
{
while (idx<q[i-1]) cout<<str[idx++]<<" ";
cout<<str[idx++]<<"\n";
}
}
cout<<"--------------------";
if (T) cout<<"\n";
}
return 0;
}
Day 3
模拟赛
满脑子都是主席树……
T1 手模几下后模出来了。二阶前缀和、式子都没问题,但没有特判外加细节处理不到位(没有给所有值的 set 加上边界),然后正解直接挂成暴力……唉
警示自己
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=2e5+5;
int n,m,a[N];
set <int> st[N];
struct BIT
{
int tr1[N],tr2[N];
int lowbit(int x) { return x&-x; }
void add(int x,int y)
{
if (!x) return ;
int yy=(1-x)*y;
while (x<N) { tr1[x]+=y; tr2[x]+=yy; x+=lowbit(x); }
}
int sum(int x)
{
int res1=0,res2=0,k=x;
while (x) { res1+=tr1[x]; res2+=tr2[x]; x-=lowbit(x); }
return res1*k+res2;
}
}bt;
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) st[i].insert(0),st[i].insert(n+1);
for (int i=1;i<=n;i++)
{
cin>>a[i];
auto id=st[a[i]].upper_bound(i),id--;
bt.add(i,i-(*id));
st[a[i]].insert(i);
}
int op,x,y,k;
while (m--)
{
cin>>op;
if (op==1)
{
cin>>x>>y;
if (y==a[x]) continue;
int pre,lst,res1,res2;
auto i=st[a[x]].upper_bound(x);
lst=*i,i--,i--,pre=*i;
i=st[y].upper_bound(x);
res2=*i,i--,res1=*i;
bt.add(lst,x-pre);
bt.add(x,pre-res1);
bt.add(res2,res1-x);
st[a[x]].erase(x),st[y].insert(x);
a[x]=y;
}
else { cin>>k; cout<<bt.sum(k)<<"\n"; }
}
return 0;
}
T2不会,写了个暴力16分。但不知道为啥全挂没了
T3想到了不带修的做法,但是没想到修改怎么维护。最后只打了个 8 分暴力。现在看来,这道题是属于正解和部分分半点关系都没有的题……
总结:推出 T1 式子的时候感觉自己有救了,但先是像上次一样忘记了 map 的存在、没有用更方便的 set 而是手写了个主席树,然后是式子推对结果上代码写反了(其实是因为那时觉得式子推错了……但其实没错)。还是码力不足,平时口胡或先看题解再写就会导致自己写的时候会漏掉一些特殊情况;以及还是一直以来的问题:细节处理不到位,看边界看边界看边界。我要练就超级码力!!!
以及,总是需要调很长时间的代码。不管是不小心敲错导致的错误,还是逻辑漏洞,有时都需要调很久。还是码力不足啊啊啊。下次一定自己主动调程序
现在的模拟赛时间分配不能像学期内那时候那样了。现在大概有能力切题,还是得把暴力(说到这,立个flag:下次调暴力时间不超过15分钟)快速打完后集中注意力去切。写完代码后也要立刻去调,不然下次再正解挂成暴力就老实了……
另外,自信一些。有些想法是对的
树的直径
可以跑两遍 dfs (不适用于负边权)或换根 dp
可合并性:两个点集合并后的直径端点一定在合并前各自直径的4个端点之中->可以用线段树维护(区间内的点构成的直径)
1.1
树的重心
以树的重心为根时,所有子树的大小不超过
有些时候以重心为根会有很好的性质
DFN序
在求所有点和给定点集中的点的最浅 LCA 时,可以只比较给出子集中 dfn 序最大的和最小的点求出 LCA
欧拉序
可以
重链剖分
好东西
一个点到根经过的链的数量是
证明:向上每经过一个轻边,子树大小至少翻倍
1.2
首先,根据调整法发现边权不重要
1.3 深度转权值后树剖维护
有个小 trick: 可以把
小声:好久没遇到这么友好的代码了www
码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=5e4+5;
const int MOD=201314;
int n,m;
vector <int> tr[N];
struct node { int val,k,id; };
vector <node> q[N];
int ans[N];
struct Segment_Tree
{
struct node
{
int l,r;
int sum,tag;
}tr[N<<2];
void push_up(int id) { tr[id].sum=(tr[id<<1].sum+tr[id<<1|1].sum)%MOD; }
void push_down(int id)
{
if (!tr[id].tag) return ;
int ls=id<<1,rs=id<<1|1;
tr[ls].sum=(tr[ls].sum+(tr[ls].r-tr[ls].l+1)*tr[id].tag%MOD)%MOD;
tr[rs].sum=(tr[rs].sum+(tr[rs].r-tr[rs].l+1)*tr[id].tag%MOD)%MOD;
tr[ls].tag+=tr[id].tag,tr[rs].tag+=tr[id].tag;
tr[ls].tag%=MOD,tr[rs].tag%=MOD;
tr[id].tag=0;
}
void build(int id,int l,int r)
{
tr[id].l=l,tr[id].r=r;
if (l==r) return ;
int mid=(l+r)>>1;
build(id<<1,l,mid),build(id<<1|1,mid+1,r);
}
void update(int id,int l,int r)
{
if (tr[id].l>=l&&tr[id].r<=r) { tr[id].sum=(tr[id].sum+tr[id].r-tr[id].l+1)%MOD; tr[id].tag++; return ; }
push_down(id);
int mid=(tr[id].l+tr[id].r)>>1;
if (mid>=l) update(id<<1,l,r);
if (mid+1<=r) update(id<<1|1,l,r);
push_up(id);
}
int query(int id,int l,int r)
{
if (tr[id].l>=l&&tr[id].r<=r) return tr[id].sum;
push_down(id);
int res=0,mid=(tr[id].l+tr[id].r)>>1;
if (mid>=l) res+=query(id<<1,l,r);
if (mid+1<=r) res+=query(id<<1|1,l,r);
return res%MOD;
}
}Tr;
int fa[N],siz[N],dep[N],son[N];
int dfn[N],tme,top[N];
void dfs1(int x,int _fa)
{
siz[x]=1,fa[x]=_fa,dep[x]=dep[_fa]+1;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs1(v,x);
siz[x]+=siz[v];
if (siz[son[x]]<siz[v]) son[x]=v;
}
}
void dfs2(int x,int _top)
{
top[x]=_top,dfn[x]=++tme;
if (!son[x]) return ;
dfs2(son[x],_top);
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==fa[x]||v==son[x]) continue;
dfs2(v,v);
}
}
void update(int x)
{
while (top[x])
{
Tr.update(1,dfn[top[x]],dfn[x]);
x=fa[top[x]];
}
}
void _query(int id,int val,int x)
{
while (top[x])
{
ans[id]=(ans[id]+val*Tr.query(1,dfn[top[x]],dfn[x])+MOD)%MOD;
x=fa[top[x]];
}
}
void solve()
{
for (int i=1;i<=n;i++)
{
update(i);
int _size=q[i].size();
for (int j=0;j<_size;j++) _query(q[i][j].id,q[i][j].val,q[i][j].k);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1,fa;i<n;i++) { cin>>fa; tr[fa+1].push_back(i+1); }
dfs1(1,0),dfs2(1,1);
Tr.build(1,1,n);
int l,r,x;
for (int i=1;i<=m;i++)
{
cin>>l>>r>>x,l++,r++,x++;
q[l-1].push_back({-1,x,i});
q[r].push_back({1,x,i});
}
solve();
for (int i=1;i<=m;i++) cout<<(MOD+ans[i])%MOD<<"\n";
return 0;
}
长链剖分
子树深度最大的子节点是长链
一个点到根的路径经过的轻边数量是
长链剖分可以优化与深度有关的 DP
树上启发式合并
维护子树信息好评
Prufer 序列
有标号无根
每次删除编号最小的叶子结点并将其父节点编号加入序列末尾
点的度数等于其在序列中出现的次数+1
? what admire 这都是些啥啊
点分治
感觉其思想和 dsu on tree 很像,都是在 LCA 统一处理
但这个大概是从上到下治……从重心开始处理,处理完后到它的子树里的重心继续处理。因为每次取的都是重心,它的子树们的大小不会超过
点分治板
板码
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e4+5;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
int q[N],ans[N],rt;
int a[N],dis[N],b[N],tol;
bool cmp(int x,int y) { return dis[x]<dis[y]; }
int siz[N],mx[N],vis[N];
void get_rt(int x,int _fa,int tol)//找重心
{
siz[x]=1,mx[x]=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa||vis[v]) continue;
get_rt(v,x,tol);
siz[x]+=siz[v];
mx[x]=max(mx[x],siz[v]);
}
mx[x]=max(mx[x],tol-siz[x]);
if (!rt||mx[x]<mx[rt]) rt=x;
}
void get_dis(int x,int _fa,int rrt)
{
a[++tol]=x;
b[x]=rrt;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa||vis[v]) continue;
dis[v]=dis[x]+tr[x][i].val;
get_dis(v,x,rrt);
}
}
void calc(int x)
{
tol=0;
a[++tol]=x;
dis[x]=0,b[x]=x;
int _size=tr[x].size();
for (int i=0;i<_size;i++)//计算子树内所有点到 x 的位置
{
int v=tr[x][i].nxt;
if (vis[v]) continue;
dis[v]=tr[x][i].val;
get_dis(v,x,v);
}
sort(a+1,a+1+tol,cmp);
for (int i=1;i<=m;i++)
{
int l=1,r=tol;
while (l<r)//双指针
{
if (dis[a[l]]+dis[a[r]]>q[i]) r--;
else if (dis[a[l]]+dis[a[r]]<q[i]) l++;
else if (b[a[l]]==b[a[r]]) dis[a[r]]==dis[a[r-1]]?r--:l++;//需在不同的子树中
else { ans[i]=1; break; }
}
}
}
void solve(int x)
{
vis[x]=1,calc(x);
int _size=tr[x].size();
for (int i=0;i<_size;i++)//处理它的子树们
{
int v=tr[x][i].nxt;
if (vis[v]) continue;
rt=0;
get_rt(v,0,siz[v]);
solve(rt);
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1,u,v,w;i<n;i++)
{
cin>>u>>v>>w;
tr[u].push_back({v,w}),tr[v].push_back({u,w});
}
for (int i=1;i<=m;i++) cin>>q[i];
mx[0]=n;
get_rt(1,0,n);
solve(rt);
for (int i=1;i<=m;i++) cout<<(ans[i]?"AYE\n":"NAY\n");
return 0;
}
树哈希
哈希函数
1.3 树同构
虚树
1.4 虚树
点分树
这种强制在线还带修的就不好跑普通的点分治了。于是出现了点分树
点分治每次沿着子树的重心分治,而点分树就是根据遍历的重心顺序建成的树。也就是说,点分树上一个点的子节点就是在原树上计算完该点后要处理的下若干个子树的重心
点分树有两个性质:
- 因为点分治的递归深度不会超过
,所以点分树的深度是 级别的 - 在点分树上的
一定在原树 的路径上,反之不一定
现在考虑怎么用点分树维护信息。设震中
和 dsu on tree 一样,统计权值和时不能统计
板
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m;
int val[N];
vector <int> tr[N];
int rt,dfa[N];
int rt1[N],rt2[N],ans;
struct Segment_Tree
{
struct node{
int ls,rs;
int sum;
}tr[N<<6];
int cnt=0;
void push_up(int id) { tr[id].sum=tr[tr[id].ls].sum+tr[tr[id].rs].sum; }
void update(int &id,int l,int r,int pos,int k)
{
if (!id) id=++cnt;
if (l==pos&&r==pos) { tr[id].sum+=k; return ; }
int mid=(l+r)>>1;
if (pos<=mid) update(tr[id].ls,l,mid,pos,k);
else update(tr[id].rs,mid+1,r,pos,k);
push_up(id);
}
int query(int id,int l,int r,int ql,int qr)
{
if (!id) return 0;
if (l>=ql&&r<=qr) return tr[id].sum;
int mid=(l+r)>>1,res=0;
if (mid>=ql) res+=query(tr[id].ls,l,mid,ql,qr);
if (mid+1<=qr) res+=query(tr[id].rs,mid+1,r,ql,qr);
return res;
}
}Tr1,Tr2;
int fa[N][20],dep[N];
void dfs(int x,int _fa)
{
fa[x][0]=_fa,dep[x]=dep[_fa]+1;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa) continue;
dfs(v,x);
}
}
inline void init()
{
for (int j=1;j<=18;j++)
for (int i=1;i<=n;i++) fa[i][j]=fa[fa[i][j-1]][j-1];
}
inline int lca(int x,int y)
{
if (dep[x]<dep[y]) swap(x,y);
int delta=dep[x]-dep[y],lg=0;
while (delta)
{
if (delta&1) x=fa[x][lg];
delta>>=1,lg++;
}
if(x==y) return x;
for (int i=18;i>=0;i--)
{
if (fa[x][i]==fa[y][i]) continue;
x=fa[x][i],y=fa[y][i];
}
return fa[x][0];
}
int mx[N],siz[N],vis[N];
void get_rt(int x,int _fa,int tol)
{
siz[x]=1,mx[x]=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (v==_fa||vis[v]) continue;
get_rt(v,x,tol);
siz[x]+=siz[v];
mx[x]=max(mx[x],siz[v]);
}
mx[x]=max(mx[x],tol-siz[x]);
if (!rt||mx[x]<mx[rt]) rt=x;
}
void div_rt(int x,int tol)
{
vis[x]=1;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i];
if (vis[v]) continue;
rt=0,get_rt(v,0,(siz[v]<siz[x]?siz[v]:tol-siz[x]));
dfa[rt]=x,div_rt(rt,(siz[v]<siz[x]?siz[v]:tol-siz[x]));
}
}
inline int dis(int x,int y) { return dep[x]+dep[y]-2*dep[lca(x,y)]; }
inline void update(int x,int k)
{
int tmp=x;
while (tmp)
{
Tr1.update(rt1[tmp],0,n-1,dis(tmp,x),k);
//为什么一定要麻烦巴拉地建两棵线段树?因为点分树上的相邻点可能相差十万八千里,不能直接在第一棵线段树上抠掉 [0,dis(tmp,x)-1]的数值因为你根本不知道他们相差是否为1相差多少……
if (dfa[tmp]) Tr2.update(rt2[tmp],0,n-1,dis(dfa[tmp],x),k);
tmp=dfa[tmp];
}
}
inline int query(int x,int y)
{
int tmp=x,pre=0,res=0;
while (tmp)
{
if (dis(tmp,x)>y) { pre=tmp; tmp=dfa[tmp]; continue ; }
res+=Tr1.query(rt1[tmp],0,n-1,0,y-dis(tmp,x));
if (pre) res-=Tr2.query(rt2[pre],0,n-1,0,y-dis(tmp,x));//扣掉x方向的
pre=tmp,tmp=dfa[tmp];
}
return res;
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1;i<=n;i++) cin>>val[i];
for (int i=1,u,v;i<n;i++)
{
cin>>u>>v;
tr[u].push_back(v),tr[v].push_back(u);
}
dfs(1,0),init();//处理LCA相关
get_rt(1,0,n),div_rt(rt,n);//建点分树
for (int i=1;i<=n;i++) update(i,val[i]);
int op,x,y;
while (m--)
{
cin>>op>>x>>y;
x^=ans,y^=ans;
if (!op) { ans=query(x,y); cout<<ans<<"\n"; }
else { update(x,y-val[x]); val[x]=y; }
}
return 0;
}
脸洗啼
《点分治好题》
本来想的是 dsu on tree ,但是这只能处理子树内的,没法求,但是淀粉质可以处理所有子树
另类点分治:若能确定更优点在某个子树内则分治递归
在这题中,显然 当贡献点对
调题过程
模拟赛摆烂后大概调了……我也不知道多长时间。大概是一些记录的点的顺序有问题
然后因为没细想,没“剪枝”,导致 T 成了50.调调调,调调调,卡常无果,去看题解
发现当且仅当只有一棵子树存在以上情况的时候需要到这棵子树内,不然倒来倒去总有一些点对会不优
然后剪完就过了 下次一定推到底
懒
#include <bits/stdc++.h>
#define pii pair<int,int>
#define mkp make_pair
#define fst first
#define scd second
using namespace std;
const int N=1e5+5;
const int inf=0x3f3f3f3f;
int n,m;
struct node { int nxt,val; };
vector <node> tr[N];
pii q[N];
int rt,ans=inf;
int mx[N],siz[N];
int b[N],dis[N],vis[N];
void get_rt(int x,int _fa,int tol)
{
siz[x]=1,mx[x]=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa||vis[v]) continue;
get_rt(v,x,tol);
siz[x]+=siz[v];
mx[x]=max(mx[x],siz[v]);
}
mx[x]=max(mx[x],tol-siz[x]);
if (!rt||mx[x]<mx[rt]) rt=x;
}
void get_dis(int x,int _fa,int fm)
{
b[x]=fm;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
if (v==_fa) continue;
dis[v]=dis[x]+tr[x][i].val;
get_dis(v,x,fm);
}
}
int calc(int x)
{
dis[x]=0;
int _size=tr[x].size();
for (int i=0;i<_size;i++)
{
int v=tr[x][i].nxt;
dis[v]=tr[x][i].val;
get_dis(v,x,v);
}
int res=0,flag=0,lst=0,cnt=0;
for (int i=1;i<=m;i++)
{
int sum=dis[q[i].fst]+dis[q[i].scd];
if (sum>res) { res=sum; cnt=0; }
if (sum==res) b[q[i].fst]==b[q[i].scd]?lst=b[q[i].fst],cnt++:flag=sum;
}
ans=min(ans,res);
if (flag==res||cnt>1) return 0;
return lst;
}
void solve(int x)
{
vis[x]=1;
int c=calc(x);
if (!c||vis[c]) return ;
rt=0,get_rt(c,0,siz[c]);
solve(rt);
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for (int i=1,u,v,w;i<n;i++)
{
cin>>u>>v>>w;
tr[u].push_back({v,w}),tr[v].push_back({u,w});
}
for (int i=1;i<=m;i++) cin>>q[i].fst>>q[i].scd;
get_rt(1,0,n),solve(rt);
cout<<ans;
return 0;
}
Day-5
再考直接爆炸
T1 非常神奇。对于
T2 难绷
T3 没想到真的和网络流有关……听不懂听不懂
线段树
线段树二分
?好腼腆的老师
区间最小未出现的自然数
显然主席树。但若是常规的“计数”,非常地不好处理。可以联想 HH 的项链中数的贡献方式:在
Day 6
不是 到底是怎么做到一分不剩全都挂掉的???
破案了,暴力数组开小了
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】