2 条题解
-
0
首先状态是简单的: 表示从 出发到达山顶的方案数。
在题目的限制下显然是无后效性的。
做一个树上前缀和,设 到 路径上 的最小值为 ,则贡献是形如 的式子。
可以发现,从下往上做是相对容易的。
考虑从下往上做,再从上往下撤销。
当前 dfs 到一个位置,用并查集维护其子树内的点到这个点路径上 的最值。
具体地, 到当前根的最值在 所在的集合的代表元处取到。
倍增预处理一个点上面第一个 小于这个点的位置,直接合并即可。
可以给每个点开个 vector,存储的信息形如 ,表示这个点的 dp 值等于 ,这样方便直接撤销。
启发式合并即可做到 。
还要特殊处理 的情况,对每个点倍增找到贡献为 的位置,直接打个 tag 即可。
总复杂度 ,数据结构仅需要用到并查集。
#include <bits/stdc++.h> using namespace std; #define int long long const int N=1e5+5,mods=998244353; int op,t,n,nw,fa[N],l[N],dy[N],r[N],mk[N],sm[N],sz[N],h[N],ff[N],d[N],f[N],dp[N],f1[N][20],f2[N][20]; vector<int>p[N],g[N],q[N],jl[N]; struct node{ int l,r,op; }; vector<node>fs[N]; int fd(int x){ if(x==ff[x])return x; return ff[x]=fd(ff[x]); } void dfs(int x){ for(int i=1;i<=18;i++){ f1[x][i]=f1[f1[x][i-1]][i-1]; f2[x][i]=min(f2[x][i-1],f2[f1[x][i-1]][i-1]); } h[x]=d[x]-h[x]-1; l[x]=d[x]-l[x]; r[x]=d[x]-r[x]; swap(l[x],r[x]); int y=x; for(int i=18;i>=0;i--)if(f2[y][i]>=h[x])y=f1[y][i]; g[fa[y]].push_back(x); y=x; for(int i=18;i>=0;i--)if(f2[y][i]>=l[x])y=f1[y][i]; if(h[x]>=l[x])jl[fa[y]].push_back(x); else jl[x].push_back(x); y=x; for(int i=18;i>=0;i--)if(f2[y][i]>=r[x])y=f1[y][i]; if(h[x]>=r[x])q[fa[y]].push_back(x); else q[x].push_back(x); for(auto c:p[x]){ d[c]=d[x]+1; f1[c][0]=x; f2[c][0]=h[x]; dfs(c); } } void init(int x){ for(auto c:p[x])init(c); sort(p[x].begin(),p[x].end(),[&](int a,int b){ return fs[a].size()>fs[b].size(); }); if(p[x].size())swap(fs[x],fs[p[x][0]]); for(int i=1;i<p[x].size();i++){ int c=p[x][i]; for(auto d:fs[c])fs[x].push_back(d),sm[x]++; } fs[x].push_back({l[x],r[x],1}); sm[x]++; for(auto c:q[x]){ fs[x].push_back({l[c],r[c],-1}); fs[x].push_back({l[c],h[x],1}); dy[c]=x; sz[x]++; sm[x]+=2; } for(auto c:jl[x]){ fs[x].push_back({l[c],h[fd(dy[c])],-1}); sz[fd(dy[c])]--; sm[x]++; } for(auto c:g[x]){ fs[x].push_back({h[x]+1,h[c],-sz[c]}); sz[x]+=sz[c]; ff[c]=x; sm[x]++; } } int gets(int l,int r){ if(l>nw)return 0; if(l>r)return 0; return dp[min(r,nw)]-dp[l-1]; } void solve(int x,int res){ if(mk[x])for(auto c:fs[x])res+=gets(c.l,c.r)*c.op,res%=mods; if(x!=1)f[x]=res; dp[d[x]]=(dp[d[x]-1]+f[x])%mods; nw=d[x]; while(sm[x]--){ auto c=fs[x].back(); fs[x].pop_back(); res-=gets(c.l,c.r)*c.op; res%=mods; } if(p[x].size()){ swap(fs[x],fs[p[x][0]]); solve(p[x][0],res); for(int i=1;i<p[x].size();i++){ int c=p[x][i]; mk[c]=1; nw=d[x]; solve(c,0); } } } signed main(){ ios::sync_with_stdio(0); cin.tie(0);cout.tie(0); cin>>op>>t; while(t--){ memset(f,0,sizeof(f)); memset(f1,0,sizeof(f1)); memset(f2,0x3f,sizeof(f2)); cin>>n; for(int i=1;i<=n;i++)ff[i]=i,mk[i]=0,sm[i]=0,sz[i]=0,p[i].clear(),g[i].clear(),fs[i].clear(),q[i].clear(),jl[i].clear(); for(int i=2;i<=n;i++){ cin>>fa[i]>>l[i]>>r[i]>>h[i]; p[fa[i]].push_back(i); } d[1]=1; dfs(1); init(1); f[1]=1; solve(1,0); for(int i=2;i<=n;i++)cout<<(f[i]%mods+mods)%mods<<" "; cout<<"\n"; } } -
0
2026.4.22:又自己重新做了一下这道好题,再补个题解。今年终于可以打 NOI 了。
先理一下限制。
我们记一次攀登为这个过程: 是某次冲刺过后的位置。从 开始休息若干次(可能为 次),滑落到 ,然后再从 冲刺一次到达 。不难发现两次攀登的限制是独立的。这里可以做到 。
然后理一下这个转移的限制:
- 是 的祖先, 是 的祖先。
- 。
- 。
再改一下第二个限制,那么对于 有限制 ,然后又有 。
那么有一个限制是和 有关的,有另外一个限制是和 都有关的,考虑先固定 。
分类讨论 和 的大小关系。
Case 1
假设 $\min_{l \in \mathrm{path}(i,j)}(d_l-h_l-1)\ge d_j-l_j$,找到最小的 满足这个条件,记为 表示这样找到的 。
那么 可以帮助 的 级祖先转移到 这些点。在 这个点的 值算完以后找到 这条链即可。
Case 2
假设 $d_j-r_j\le \min_{l \in \mathrm{path}(i,j)}(d_l-h_l-1)< d_j-l_j$。
这个情况看起来就比较复杂,而且在变 的同时改变贡献范围,看起来有点魔怔。
这个 case 中 被 贡献了, 是 到根的前缀和。 和 具体是什么不重要,但是 是固定的,所以我们可以先扔掉 。
那么现在一对 的贡献:
- 首先要求 是 的严格祖先,并且 是 的祖先。 是什么不用管。
- 此外, 的 级祖先的 值是这次贡献的系数。
想一下这是啥意思啊。区间 的形态让我们想到一点笛卡尔树,枚举了几个做法以后发现我们应该让 的最小值和这个最小值所在的最浅位置 挂钩,具体的:
- 一个点 有 表示 的祖先中,第一个比 小的数。记这棵树为 。
- 的最小值是 一直往上跳后跳到的结果。
考虑 这个路径上(不含 )的点 ,都可以被 在 树上的子树贡献。
预处理一下子树大小,就可以在 的祖先的 值都被算完后,立刻给 都贡献上 的 的贡献,并乘上合法的 的数量。
这个数量是多少? 对 的限制可以简单的看成深度限制,预处理 子树内的数据结构,查一次即可。
Case 3
假设 $\min_{l \in \mathrm{path}(i,j)}(d_l-h_l-1)< d_j-r_j$。
这个大家应该都会做。
总结
树链加法可以树上差分,拆成单点加,子树查。
时间复杂度 。
下面是 2024.7.25 通过的题解:
免责声明:场外选手纯凑热闹。我只是一个 CSP-S 2= 的准退役选手别骂我呜呜呜,从描述就可以看出作者可能会讲的比较初等,请见谅。
“为什么要攀登?因为山就在那里。”
下文定义 $\displaystyle s_i = d_i - h_i - 1,p_{i,j}=\min_{k \in \mathrm{path}_{i,j}}s_k$。
设 为点 到山顶的方案数。简单分析一下,可以发现肯定是先向下走一段距离到一个点 ,再突然跳到一个 的祖先(由题目的另一个描述可以知道这同样是 的祖先) 上。一路上经过了 到 路径上的所有点还有 点。有图为证:

我们为了求出 ,需要用 贡献。仔细读题后发现, 的两个条件都比较强,一个是 ,另一个是 。
但是我们发现这两个条件都和 有关,而且根据一大堆题目的经验,我们从 点开始考虑。
计算出每一个点到根节点的 的总和 。因为 是 的祖先上的一个区间,所以采用前缀和,要加在 上的答案变为 。
分类讨论点 的位置。

如图,红点是不能被算入贡献()的,黄点是 的。绿点是 的。可以发现红黄绿三种点一定是如图排列的。
的贡献,用倍增找到黄绿点以后可以直接链加上去。下文考虑 。
绿点很好处理,用倍增找到以后可以直接链加上去。
黄点比较难。考虑枚举从 到 ,前缀最小值的位置(如果有多个请取深度最大的那个),请观察以下的图片(由一条链沿着叶子到根的方向拍下来)。

其中, 会一直到一个比当前这个前缀最小值更小的位置(但是,不能越过绿点的界线,也就是 )。 点同理。所以,我们可以随便用数据结构维护 的个数,然后直接链加到每个 上。
这里有许多的链加操作,事实上不需要树链剖分,只需要差分一下然后查询的时候查子树和就可以了。用 dfs 序,写一个树状数组即可。
另外有一个细节就是因为我们不能等枚举到 了才做这些操作(因为 是 的祖先)。所以我们要把 的每一个限制()给离线,然后当 的数据算出时立马贡献。当然,前缀最小值 同理。
总结:很厉害,学到了很多。
但是作者太菜了,而且怕给其他人带来不便,所以不放代码了。
本题解参考了其他题解,主要用于自己总结用,并且另外补充了一下图解。
- 1
信息
- ID
- 7392
- 时间
- 2000ms
- 内存
- 2048MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者