T1:还在头铁,顺便复习了一下lct【虽然这题用不上因为复杂度不对】
头铁结束。
虽然题目存在换根的操作,实际上并不用真的换根。
2操作中求lca的时候只要考虑原树上root和x、y的lca以及x,y的lca,三个中取最深的就是现树上x和y的lca。
关于u的子树整体操作需要分类讨论。如果现根不在原树上u的子树里,那么在新树上的目标子树与原树相同,直接操作。如果u就是root,那么直接整棵树都操作。最后如果root在原树上u子树里,那么需要反一下,应当进行操作的部分是整棵树上除了u包含root的这棵子树的所有部分。
查询操作同理。
树上一棵子树的整体加减可以利用树剖解决。一整棵子树对应在线段树上是一段连续区间。
#include<iostream> #include<cstdio> using namespace std; int n,q,a[300010],root=1,t; int ver[600010],Next[600010],head[300010],tot; int siz[300010],dep[300010],son[300010],rec[300010],rec1[300010]; int fa[300010],lip[300010],cnt; void add(int x,int y){ ver[++tot]=y; Next[tot]=head[x]; head[x]=tot; } struct node{ int l,r; long long sum,tag; }b[300010*4]; void dfs(int x,int f){ siz[x]=1; for(int i=head[x];i;i=Next[i]){ int y=ver[i]; if(y==f)continue; dep[y]=dep[x]+1; fa[y]=x; dfs(y,x); siz[x]+=siz[y]; if(siz[y]>siz[son[x]])son[x]=y; } } void dfs1(int x,int upp){ lip[x]=upp; rec[x]=++cnt; rec1[cnt]=x; if(!son[x])return; dfs1(son[x],upp); for(int i=head[x];i;i=Next[i]){ int y=ver[i]; if(y==fa[x]||y==son[x])continue; dfs1(y,y); } } void build(int p,int l,int r){ b[p].l=l,b[p].r=r; if(l==r){ b[p].sum=a[rec1[l]]; return; } int mid=(l+r)/2; build(p*2,l,mid); build(p*2+1,mid+1,r); b[p].sum=b[p*2].sum+b[p*2+1].sum; } int lca(int x,int y){ while(lip[x]!=lip[y]){ if(dep[lip[x]]<dep[lip[y]])swap(x,y); x=fa[lip[x]]; } if(dep[x]>dep[y])return y; else return x; } int lca1(int x,int y){ while(lip[x]!=lip[y]){ if(fa[lip[y]]==x)return lip[y]; y=fa[lip[y]]; } return rec1[rec[x]+1]; } void pushdown(int p){ if(b[p].tag){ b[p*2].tag+=b[p].tag; b[p*2].sum+=(b[p*2].r-b[p*2].l+1)*b[p].tag; b[p*2+1].tag+=b[p].tag; b[p*2+1].sum+=(b[p*2+1].r-b[p*2+1].l+1)*b[p].tag; b[p].tag=0; } } void change(int p,int l,int r,long long y){ if(l<=b[p].l&&b[p].r<=r){ b[p].sum+=(b[p].r-b[p].l+1)*y; b[p].tag+=y; return; } pushdown(p); int mid=(b[p].l+b[p].r)/2; if(l<=mid)change(p*2,l,r,y); if(r>mid)change(p*2+1,l,r,y); b[p].sum=b[p*2].sum+b[p*2+1].sum; } long long ask(int p,int l,int r){ if(l<=b[p].l&&b[p].r<=r){ return b[p].sum; } pushdown(p); int mid=(b[p].l+b[p].r)/2; long long val=0; if(l<=mid)val+=ask(p*2,l,r); if(r>mid)val+=ask(p*2+1,l,r); b[p].sum=b[p*2].sum+b[p*2+1].sum; return val; } int main(){ // freopen("1.in","r",stdin); // freopen("1.out","w",stdout); scanf("%d%d",&n,&q); for(int i=1;i<=n;i++){ scanf("%d",&a[i]); } for(int i=1,x,y;i<n;i++){ scanf("%d%d",&x,&y); add(x,y),add(y,x); } dfs(1,0); dfs1(1,1); build(1,1,n); // scanf("%d",&q); for(int i=1,opt,x,y;i<=q;i++){ long long z; scanf("%d",&opt); if(opt==1){ scanf("%d",&x); root=x; } else if(opt==2){ scanf("%d%d%lld",&x,&y,&z); int x1=lca(x,y); t=x1; int x2=lca(x,root); if(dep[x2]>dep[t])t=x2; int x3=lca(y,root); if(dep[x3]>dep[t])t=x3; if(lca(t,root)!=t){//t原子树 change(1,rec[t],rec[t]+siz[t]-1,z); } else if(t==root){ b[1].sum+=n*z; b[1].tag+=z; } else{ b[1].sum+=n*z; b[1].tag+=z; int t1=lca1(t,root); change(1,rec[t1],rec[t1]+siz[t1]-1,-z); } } else{ scanf("%d",&x); t=x; if(lca(t,root)!=t){//t原子树 printf("%lld ",ask(1,rec[t],rec[t]+siz[t]-1)); } else if(t==root){ printf("%lld ",b[1].sum); } else{ long long val=b[1].sum; int t1=lca1(t,root); val-=ask(1,rec[t1],rec[t1]+siz[t1]-1); printf("%lld ",val); } } } return 0; }
T2:Function
看到题目里的递推式,莫名其妙先画了个网格,横x轴纵y轴。
然后发现第一排是a1的各个倍数,第一列是ai自己。然后根据相邻两个a的大小关系好像可以推路径。
然后写暴力打了个表,按普通的行列输出,发现自己这部分想麻烦了。似乎前面小的ai可以斜着连过来覆盖现在的ai,一列会分段被多个ai覆盖。然后感觉有点像决策单调性【什么】,试图从前面转移。
然后考虑给一列打标记,在哪个x开始被前面的哪个a覆盖,然后每一列从上一列转移这个标记。然后发现中间有断层,行不通。
这个时候重新开始想之前发现的斜着连过来这一块,一个(x,y)可以先向之前的ai的列去走,然后到某一列一直走到顶。
于是怎么找这个ai?
把走到ai这列得出的答案列了个式子:ai*(x-y)+s[y]-s[i]+i*a[i]。假设从(x,y)往回一直走到第一列,还剩下x-y步到顶,这部分一定要填ai。然而ai不一定是第一列,多往回走几列就少让ai累加了多少行,再把这部分补上。以及第一行就是ai本身而不是从0开始所以相当于ai多累加一行。
对于一个询问,sy是不变的,所以先去掉。发现剩下的这部分式子是一个关于x-y的一次函数。假设a=ai,b=-s[i]+i*a[i]。
我对斜率优化不熟…在这里开始懵逼了,最后强行头铁出了单调栈这一步但是写不完了。
/*(注释掉了)
推出一次函数以后,考虑ai比aj优的条件。两个式子用不等式算一下得出i比j优的条件是(bj-bi)/(ai-aj)>(x-y),两个ai覆盖的交点也可以这样得到。
维护一个下凸包,斜率递增。
观察打好的表发现,覆盖同一列y的ai从靠近y的地方递减。离y远而a更大的ai没有再做贡献的可能性,即作贡献的ai递减。
所以可以用单调栈维护。询问的时候在凸壳上二分x的位置。
*/
出锅了出锅了,方法没锅思路有锅。
之前有点强行解释的嫌疑。实际上得出一堆一次函数以后,因为我们只考虑这些一次函数的最小值,所以才会去维护一个上凸包。
上凸包!!被机房大佬无情嘲笑。画张图一看我脑子里这就是个上凸包…
用两个点的函数式进行比较的函数单纯只是为了维护这个凸包而已和什么x的覆盖到底有没有关系我也搞不清。但是不用这种写法,老老实实在凸包上二分,当然完全是正确的。
二次补充:
的确是上凸包,这个没问题。
每次到新的一列加一条直线,其实是加了一条从原点开始斜率ai的直线。因为每次转移到下一列的时候凸包其实整体向右和向上平移了【可能就是我前面发现的多端覆盖标记的后移】,所以对于新加入的这条原点开始的直线来说,它能ban掉所有之前的斜率比它大的直线,即aj>ai且j<i的直线。
这就是弹出大于当前ai的a值的意义。
而后面利用函数比较而弹栈的操作,有一种更好理解的做法【也许等价?】:解方程得栈顶与新加入直线的交点以及栈顶与前一条直线的交点,弹掉所有会让凸包交点不单调的直线。
#include<iostream> #include<cstdio> #include<algorithm> using namespace std; int n,q,cnt=1; long long a[1000010],sum[1000010],b[1000010],ans[1000010]; double num[1000010]; int t[1000010],p; struct node{ int x,y,id; }que[1000010]; bool cmp(node a,node b){ if(a.y==b.y)return a.x<b.x; else return a.y<b.y; } double get(int x,int y){ return (double)(b[y]-b[x])/(double)(a[x]-a[y]); } int main() { // freopen("1.in","r",stdin); // freopen("1.out","w",stdout); scanf("%d",&n); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); sum[i]=sum[i-1]+a[i]; b[i]=i*a[i]-sum[i]; } scanf("%d",&q); for(int i=1;i<=q;i++){ scanf("%d%d",&que[i].x,&que[i].y); que[i].id=i; } sort(que+1,que+q+1,cmp); for(int i=1;i<=n;i++){ while(p&&a[t[p]]>=a[i])p--; while(p>=2&&get(i,t[p])>get(t[p],t[p-1]))p--; t[++p]=i; if(p>1)num[n-p]=get(i,t[p-1]); while(que[cnt].y==i){ int x=lower_bound(num+n-p,num+n-1,(que[cnt].x-que[cnt].y))-num; x=n-x; ans[que[cnt].id]=a[t[x]]*(que[cnt].x-que[cnt].y)+b[t[x]]+sum[que[cnt].y]; cnt++; } } for(int i=1;i<=q;i++){ printf("%lld ",ans[i]); } return 0; }
有些地方自己也不是很明白,再找一点斜率优化的题写…
二次补充结束,终于算是全搞明白了……
T3:什么FT?FF什么?F什么T?
因为头铁T2而挂掉了的考试,最后发现T2其实我也没推出来多少,主要是自创斜率优化【指不熟斜率优化】的操作属实弟中弟。
今天没有考试,但我改不完【迫真】