九月晚练总结(2026)
9.1 Zebra Crossing
贪心题,感觉我贪心太烂了。
首先我们将跳的过程看做一步一步跳,但是跳的步数上限是 \(k\),这样会好搞一些。
我们考虑从当前点跳到下一个点时的情况,假设当前我还有 \(step\) 步可以走,那么显然下一个点就有 \(step-1\) 步可以走,但是我们可以选择先走到一个白点再走回来。显然选择最近的那个白点肯定更优,记 \(dis_i\) 表示离第 \(i\) 个点最近的白点与 \(i\) 的距离,那么我们就可以算出第二种情况下的 \(step\) 可以更新为 \(k-dis_i\)。
如果在进行了上面两轮更新后发现走到下一个点时的 \(step\) 等于 \(0\) 了,显然我们只能强制落地一次,那就将这次落地做的贡献算上就行了。
思路还是比较简单的,只是第一步转化有点难想。
代码:
const int N=5e5+6;
int n,k,a[N],dis[N],ans[N];
vector<int>v[N];
void bfs()
{
queue<int>q;
for(int i=1;i<=n;i++)
{
dis[i]=1e18;
if(!a[i])
{
dis[i]=0;
q.push(i);
}
}
while(!q.empty())
{
int u=q.front();
q.pop();
for(auto i:v[u])
{
if(dis[i]>dis[u]+1)
{
dis[i]=dis[u]+1;
q.push(i);
}
}
}
}
void dfs(int x,int fa,int s,int cnt,int ex)
{
ans[x]=cnt+ex;
for(auto i:v[x])
{
if(i==fa)
{
continue;
}
int ns=s-1;
int num=cnt;
int e=a[i];
if(dis[i]<=ns)
{
ns=max(ns,k-dis[i]);
}
if(ns==0)
{
if(a[i])
{
num=cnt+1;
}
ns=k;
e=0;
}
dfs(i,x,ns,num,e);
}
}
signed main()
{
int T=read();
while(T--)
{
n=read(),k=read();
for(int i=1;i<=n;i++)
{
char c;
cin>>c;
a[i]=(c-'0')^1;
}
for(int i=1,x,y;i<n;i++)
{
x=read(),y=read();
v[x].push_back(y);
v[y].push_back(x);
}
bfs();
dfs(1,0,k,0,0);
write(ans+2,ans+n+1);
putchar('\n');
for(int i=1;i<=n;i++)
{
v[i].clear();
}
}
return 0;
}
9.2 Triangle Collection
说句闲话:之前要是这样天天做紫题我可能会很累,现在居然感觉还好,感觉是去八中给磨成这样了。
又是神秘结论题,我依然没瞪出来结论,感觉部分分设计的并不是很合理。
下面讲一种我个人感觉比较自然的思路。
首先我们会看到等腰三角形需要用两条腰去匹配一个底,这个底显然并不好找,我们考虑从腰入手。
我们考虑如果一种长度的木棍个数为偶数个,那么显然它可以不断取出一对腰去和底匹配,直到木棍全部用完或没法再匹配为止,那么如果此时在它的匹配范围内有一种奇数个数的木棍长度,那么显然我们可以用一对腰去和那额外的一根进行匹配从而让那种木棍的个数也为奇数。
那如果所有木棍个数都是偶数了呢?
此时需要我们手玩几组数据会发现一个结论:如果所有木棍种类的个数都是偶数,那么最终答案必然是木棍总数除以 \(3\) 向下取整。
来自另一篇题解的证明:
我们先取出三对木棍,每对木棍的长度分别为 \(a,b,c(a\le b\le c)\),那么我们会想到这六根木棍的匹配方法有 \((b,b,a)\) 和 \((c,c,a)\) 两种。
因此我们可以不断取三对木棍出来,最终必然只会剩下 \(0\) 或 \(1\) 或 \(2\) 对,除了还剩 \(2\) 对木棍的情况可以再额外凑出一种,其余情况都达到了上限,然后你计算一下这三种情况时的总木棍数分别为多少,算一下就会发现这个结论是正确的。
稍稍简化了一下。
最后我们再来考虑如何匹配奇数个数的木棍,因为显然把奇数个数的木棍变成偶数个数肯定比直接让所有偶数个数的木棍互相匹配更优,所以我们考虑取出一些偶数个数的木棍和奇数个数的木棍匹配。这个感觉有点像 Hall 定理,不过我们可以用另一种思路做。
我们可以把一根木棍看做左括号,把一对木棍看做右括号,于是这个问题就变成了括号匹配问题。根据范围限制,我们需要在第 \(i\) 个位置插入一个左括号,在第 \(2i-1\) 个位置插入一个右括号。如果两个括号放在了同一个位置,那么左括号在前。
为了方便,我们可以把左括号的位置统一换成 \(2i-1\),右括号的位置统一换成 \(2i\)。
代码:
const int N=2e5+6;
int n,q,a[N];
struct info{
int pre,suf,cnt;
void clear()
{
pre=suf=cnt=0;
}
};
info operator + (info x,info y)
{
info b;
int s=min(x.suf,y.pre);
b.cnt=x.cnt+y.cnt+s;
b.pre=x.pre+y.pre-s;
b.suf=x.suf+y.suf-s;
return b;
}
struct seg_tree{
struct Node{
int l,r;
info f;
}node[N<<4];
void build(int l,int r,int pos)
{
node[pos].l=l,node[pos].r=r,node[pos].f.clear();
if(l==r)
{
return;
}
int mid=l+r>>1;
build(l,mid,pos<<1);
build(mid+1,r,pos<<1|1);
}
void pushup(int pos,int ls,int rs)
{
node[pos].f=node[ls].f+node[rs].f;
}
void update(int pos,int x,info y)
{
if(node[pos].l==node[pos].r)
{
node[pos].f=y;
return;
}
int mid=node[pos].l+node[pos].r>>1;
if(x<=mid)
{
update(pos<<1,x,y);
}
else
{
update(pos<<1|1,x,y);
}
pushup(pos,pos<<1,pos<<1|1);
}
}tr;
signed main()
{
n=read(),q=read();
read(a+1,a+n+1);
tr.build(1,4*n,1);
int sum=0;
for(int i=1;i<=n;i++)
{
tr.update(1,2*i-1,(info){0,a[i]&1,0});
tr.update(1,4*i-2,(info){a[i]>>1,0,0});
sum+=(a[i]>>1);
}
while(q--)
{
int x,y;
x=read(),y=read();
sum-=(a[x]>>1);
a[x]+=y;
sum+=(a[x]>>1);
tr.update(1,2*x-1,(info){0,a[x]&1,0});
tr.update(1,4*x-2,(info){a[x]>>1,0,0});
int s=tr.node[1].f.cnt;
write(s+(2*sum-2*s)/3);
putchar('\n');
}
return 0;
}
9.3 the cure
同学出的题,还是太强了膜拜膜拜。
首先所有问题我们都先考虑能不能转化为判定型问题。这道题显然可以二分答案。
考虑如何构造出当前的答案,显然一种构造方法是先把小于当前数的所有数填进去,剩下的数再通过题目操作搞出来。这是正推的想法,但是你会发现正推会出现自我套用的情况,也就是我要构造出当前这个数,需要一个更小的数,而构造这个更小的数,需要一个更小的数。因此我们考虑倒推。
如果我要构造出 \(x\),那就必须构造出 \(0\sim x-1\),我们先假设这些数我们全都构造出来了,那么每个数我们就需要至少一个,考虑 \(x-1\),如果我们能直接提取出一个 \(x-1\) 那就直接提取,多余的可以变为 \(0\) 留着后面用。如果提取不出来那么我们就需要 \(0\sim x-2\) 来构造一个 \(x-1\),那么后面所有的的数就至少需要两个了。
所以我们可以记录当前后面的数需要的个数 \(der\) 和我们将多少个数变为了 \(0\),最后只需要看看 \(0\) 有的个数和实际需要的个数的大小关系就行。
代码:
const int N=1e6+6;
int n,a[N],cnt[N];
bool check(int x)
{
if(!x)
{
return true;
}
int sum=cnt[0];
for(int i=x;i<=n;i++)
{
sum+=cnt[i];
}
int der=1;
for(int i=x-1;i>=1;i--)
{
if(cnt[i]>=der)
{
sum+=cnt[i]-der;
}
else
{
der+=der-cnt[i];
}
if(der>n)
{
return false;
}
}
return der<=sum;
}
signed main()
{
int T=read();
while(T--)
{
n=read();
read(a+1,a+n+1);
if(n==1)
{
write(a[1]);
putchar('\n');
continue;
}
for(int i=1;i<=n;i++)
{
cnt[a[i]>n?0:a[i]]++;
}
int l=1,r=n,ans=0;
while(l<=r)
{
int mid=l+r>>1;
if(check(mid))
{
ans=mid;
l=mid+1;
}
else
{
r=mid-1;
}
}
write(ans);
putchar('\n');
for(int i=0;i<=n;i++)
{
cnt[i]=0;
}
}
return 0;
}
9.4 A Ribbon for Tomorrow
首先你手玩一下样例就会发现不管如何翻转,这个字符串中连续 \(0\) 段的数量和连续 \(1\) 段的数量是永远不变的。因此我们可以考虑将一整段 \(0\) 或一整段 \(1\) 合并在一起。
然后你再手玩几组数据会发现:一次翻转相当于改变了两个颜色相同的且关于这次翻转范围的正中间对称的连通块的大小,所以说白了这个翻转本质上就是在改变序列中颜色相同的连通块的大小,而每一块的大小是一个任意的正整数。
于是这个问题变成了如果我有 \(cnt1\) 个 \(1\),要求你把它划分成 \(cnt\) 段,且每一段都至少有一个数,有多少种划分方式。这个答案显然是 \(\binom{cnt1-1}{cnt-1}\)。
\(0\) 也是类似的。最终答案就是这两个答案相乘。
代码:
const int N=1e6+6,mod=998244353;
int n,a[N],fac[N],inv[N];
int C(int x,int y)
{
if(x<0||y<0)
{
return 1;
}
return fac[x]*inv[y]%mod*inv[x-y]%mod;
}
int qpow(int x,int y)
{
int z=1;
while(y)
{
if(y&1)
{
z=z*x%mod;
}
x=x*x%mod;
y>>=1;
}
return z;
}
signed main()//26min
{
int T=read();
while(T--)
{
n=read();
fac[0]=1;
for(int i=1;i<=n;i++)
{
fac[i]=fac[i-1]*i%mod;
}
inv[n]=qpow(fac[n],mod-2);
for(int i=n-1;i>=0;i--)
{
inv[i]=inv[i+1]*(i+1)%mod;
}
for(int i=1;i<=n;i++)
{
char c;
cin>>c;
a[i]=c-'0';
}
int cnt0=(a[1]==0),cnt1=(a[1]==1),len0=(a[1]==0),len1=(a[1]==1);
for(int i=2;i<=n;i++)
{
if(a[i]!=a[i-1])
{
cnt0+=(a[i]==0);
cnt1+=(a[i]==1);
}
len0+=(a[i]==0);
len1+=(a[i]==1);
}
write(C(len0-1,cnt0-1)*C(len1-1,cnt1-1)%mod);
putchar('\n');
}
return 0;
}
9.4 零段积分
你告诉我我要手推一个什么玩意儿(关于我手推公式推了 \(3\) 个小时这一块)?
首先我们会发现我们只关心两个相邻的连续 \(0\) 段的长度。因此我们可以先枚举 \(i,j\) 表示这两个相邻连续 \(0\) 段的长度。
那么我们会知道这两个 \(0\) 段中间必然有一个连续 \(1\) 段,所以我们再枚举一个 \(k\) 表示这个 \(1\) 段的长度。
那么显然我们还剩下 \(n-i-j-k\) 个数,每个数都可以是 \(0\) 或 \(1\),所以方案数为 \(2^{n-i-j-k}\)。
现在我们要把这个“010”的结构插入到剩下的 \(n-i-j-k\) 个数当中去,我们会发现一个问题:你插入的位置两边如果有 \(0\),那么上面的公式就都错了。
因此我们来分类讨论一下:
- 如果这个结构正好插在中间,那么我们只需要强制在这个结构两边加上两个 \(1\),这样它就可以被放在任何位置了。此时的总贡献是 \(2^{n-i-j-k-2}(n-i-j-k-1)\times i\times j\)。
- 如果这个结构正好插在两边,那么我们只需要其中一边是个 \(1\) 就行,因为它可以被放在左边或右边,所以贡献要乘 \(2\),此时总贡献为 \(2^{n-i-j-k-1}\times i\times j\times 2\)。
- 如果这个结构两边都没有数,说明此时整个序列就是“010”这种结构,那么我们只需要枚举 \(i\) 和 \(j\),此时的贡献为 \(i\times j\)。
所以我们可以写出总贡献:
当然现在我们还没把概率放进去。
为了方便,我们记 \(p=\frac{P}{Q},q=1-p\),也就是出现 \(1\) 的概率和出现 \(0\) 的概率。
因为我们并不知道具体有多少个 \(1\),所以我们还需要再枚举一层:
稍微有点长。
你会发现出现了很多次 \(i+j\) 这种结构,而真正要区分 \(i\) 和 \(j\) 的时候仅仅是算 \(i\times j\),你意识到如果已经确定 \(i+j\),那么除了 \(i\times j\) 这一项之外的别的值全都不变,所以我们可以记一个 \(w(sum)=\sum_{i=1}^{sum-1}i(sum-i)\),我们可以得到递推式:\(w(sum+1)=\sum_{i=1}^{sum}i(sum+1-i)=\sum_{i=1}^{sum-1}i(sum-i)+sum\times0+\sum_{i=1}^{sum}i=w(sum)+\frac{sum(sum+1)}{2}\)。
于是上面的式子就变成了:
好的,复杂度降到了 \(O(n^3)\)。为了方便,下面所有的式子中均以 \(i\) 来替代 \(sum\)。
稍微做一下整理,把只跟 \(i\) 有关的提出来:
我们发现最后一项可以直接算了,因此先暂时忽略掉。
现在最大的问题是 \(\sum_{l=0}^{n-i-k-2}p^{k+l+2}q^{n-k-l-2}\binom{n-i-k-2}{l}\) 这个东西怎么优化。
为了方便,我们设 \(N=n-i-k-2\),那么我们得到:
把跟 \(l\) 无关的东西先扔出去,得到:
稍微整理一下得到:
你仔细观察后面那一项,是不是长的像二项式定理?我们套进去得到:
因为我们知道 \(p+q=1\),所以有:
代回去得到:
后面那堆东西同理。
所以我们可以把上面那个式子优化为:
最后再来考虑优化那个循环。我们会发现我们可以把循环拆成这样:
所以我们只需要 \(O(n)\) 预处理 \(\sum p^{k+2},\sum kp^{k+2},\sum p^{k+1}\) 这些东西就行了。当然第一项和第三项可以合并为 \(\sum p^k\) 来预处理。
所有的幂次可以 \(O(1)\) 求出,所有递推式均可 \(O(n)\) 求出,总复杂度 \(O(n)\)。
代码:
const int N=1e6+6,mod=1e9+7;
int n,P,Q,pwp[N],pwq[N],w[N],s1[N],s2[N];
int qpow(int x,int y)
{
int z=1;
while(y)
{
if(y&1)
{
z=z*x%mod;
}
x=x*x%mod;
y>>=1;
}
return z;
}
int inv(int x)
{
return qpow(x,mod-2);
}
signed main()
{
n=read(),P=read(),Q=read();
int p=P*inv(Q)%mod,q=(1-p+mod)%mod;
pwp[0]=pwq[0]=1;
for(int i=1;i<=n;i++)
{
pwp[i]=pwp[i-1]*p%mod;
pwq[i]=pwq[i-1]*q%mod;
}
for(int i=1;i<=n;i++)
{
w[i]=(w[i-1]+i*(i-1)/2%mod)%mod;
s1[i]=(s1[i-1]+pwp[i])%mod;
s2[i]=(s2[i-1]+i*pwp[i+2]%mod)%mod;
}
int ans=0;
for(int sum=1;sum<=n-3;sum++)
{
ans=(ans+((n-sum-1)*((s1[n-sum]-s1[2]+mod)%mod)%mod-s2[n-sum-2]+mod)%mod*w[sum]%mod*pwq[sum]%mod)%mod;
}
for(int sum=1;sum<=n-2;sum++)
{
ans=(ans+w[sum]*2%mod*((s1[n-sum]-s1[1]+mod)%mod)%mod*pwq[sum]%mod)%mod;
}
for(int sum=1;sum<n;sum++)
{
ans=(ans+w[sum]*pwp[n-sum]%mod*pwq[sum]%mod)%mod;
}
write(ans);
return 0;
}
9.5 Distinct GCDs (Hard Version)
我只能说没做过这种 trick 的人真的做不出来这道题。
题目让我们在保证相邻两项的 \(\gcd\) 不同的情况下让每个数出现尽可能多次,这里我们可以将其转化为图上问题。
我们考虑构造这样一张完全图:每条边的两个端点就是相邻两项,而这条边的权值就是这两项的 \(\gcd\),那么我们只需要在图上走欧拉回路就能构造出一个合法的序列。
我们算算一张 \(m\) 个点的完全图的欧拉回路长度:
- 当 \(m\) 为奇数时,此时每个点的度数都是偶数,因此长度就是 \(\frac{m(m-1)}{2}\),经过的点数就是 \(\frac{m(m-1)}{2}+1\),因为每个数可以再复制自己一份,所以能构成的序列长度为 \(\frac{m(m-1)}{2}+m+1\)。
- 当 \(m\) 为偶数是,此时每个点的度数为奇数,所以我们需要删除 \(\frac{m-2}{2}\) 条点集不重的边,此时总长度为 \(\frac{m(m-1)}{2}-\frac{m-2}{2}+m+1\)。
考虑如何构造使得每条边的边权都不一样。一种比较显然的构造方法是第 \(x\) 个点的权值我们就赋为 \(2^{x-1}3^{m-x}\),其中 \(m\) 是点数,这样显然任意两条边的边权都必然不一样。
但是通过计算我们可以得到:当 \(n=5000\) 时,我们需要 \(100\) 个点才能构造出这么长的序列,而 \(100\) 个点显然权值已经超出了 \(10^{18}\)。
因此我们考虑将一个数 \(x\) 拆成 \(iM+j\) 的形式,然后我们可以用 \(2^i3^{M-i}5^j7^{M-j}\) 这种东西进行构造。
但是如果你以 \(i\) 为行,\(j\) 为列画出一个计算的表格,那么你会发现一个矩形的左上角和右下角的 \(\gcd\) 跟左下角和右上角的 \(\gcd\) 是相同的。因此我们可以单独添加一个 \(11^{i+j}\) 来辅助构造。
这样结果可能会有点大,把质数之间换一下就行了。
代码:
const int N=5e3+6,M=106;
int n,m,pw2[26],pw3[16],pw5[16],pw7[16],pw11[16],now[M],ans[N];
bool vis[M],mp[M][M];
stack<int>st;
int calc(int x)
{
if(x&1)
{
return x*(x-1)/2;
}
else
{
return x*(x-1)/2-(x-2)/2;
}
}
void dfs(int x)
{
for(int i=now[x];i<=m;i++)
{
now[x]=i+1;
if(mp[x][i])
{
mp[x][i]=mp[i][x]=0;
dfs(i);
}
}
st.push(x);
}
signed main()
{
pw2[0]=pw3[0]=pw5[0]=pw7[0]=pw11[0]=1;
for(int i=1;i<=20;i++)
{
pw2[i]=pw2[i-1]*2;
}
for(int i=1;i<=10;i++)
{
pw3[i]=pw3[i-1]*3;
pw5[i]=pw5[i-1]*5;
pw7[i]=pw7[i-1]*7;
pw11[i]=pw11[i-1]*11;
}
int T=read();
while(T--)
{
n=read();
m=1;
while(calc(m)+m+1<n)
{
m++;
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=m;j++)
{
if(i!=j)
{
mp[i][j]=1;
}
else
{
mp[i][j]=0;
}
}
}
if(!(m&1))
{
for(int i=3;i<=m;i+=2)
{
mp[i][i+1]=mp[i+1][i]=0;
}
}
for(int i=1;i<=m;i++)
{
now[i]=1;
}
st=stack<int>();
dfs(1);
for(int i=1;i<=m;i++)
{
vis[i]=0;
}
int num=0;
while(!st.empty())
{
int x=st.top();
st.pop();
int i=(x-1)/10+1,j=x%10+1;
ans[++num]=pw2[i+j-2]*pw3[i-1]*pw5[10-i]*pw7[j-1]*pw11[10-j];
if(!vis[x])
{
ans[++num]=pw2[i+j-2]*pw3[i-1]*pw5[10-i]*pw7[j-1]*pw11[10-j];
vis[x]=1;
}
if(num>n)
{
break;
}
}
write(ans+1,ans+n+1);
putchar('\n');
}
return 0;
}
9.27 werewolf 狼人
中间补文化课去了。
方向对了,但是办法错了。
首先显然我们整个路径一定是长成从起点到某个点再到终点的过程,因此我们可以反过来考虑成同时从起点和终点出发,最终要汇合到一个点上。或者换句话说:从起点出发能到的点的集合与从终点出发能到的点的集合是否有交集。
因此这就变成了一个类似于判断连通性的问题,判断连通性最简单直接的办法就是 DSU,但是这里显然没法使用。另外,如果遇到前缀(后缀)限制时的连通性问题,我们可以选择使用 kruskal 重构树。
具体怎么操作呢,以人形为例:你要求走到的每个点编号不能小于 \(L\),那么我们给每条边赋一个权值为这条边的起点与终点中编号更小的点的编号,那么我们按照边权从大到小来建生成树,此时这个限制就变成了重构树上的一棵子树。狼形也是同理。
因此这个问题又被我们转化成了:已知两棵树,求这两棵树中的点的集合是否有交集。这个就是经典问题了,我们可以使用 dfs 序,然后在扫一棵树的同时将这个点在另一棵树上的 dfs 序标记一下,从而计算出交集大小。
当然在本题中是一棵子树,所以你可以选用扫描线(离线)或主席树(在线)的方法做到只考虑单个子树。
代码:
const int N=2e5+6;
struct edge{
int st,ed;
}e[N<<1];
int n,m,q;
struct DSU{
int fa[N<<1];
void init(int x)
{
for(int i=1;i<=x;i++)
{
fa[i]=i;
}
}
int find(int x)
{
return fa[x]=(fa[x]==x?x:find(fa[x]));
}
void merge(int x,int y)
{
int fx=find(x),fy=find(y);
if(fx==fy)
{
return;
}
fa[fy]=fx;
}
}dsu;
struct kruskal{
int num,cnt,a[N<<1],dfn[N<<1],id[N<<1],R[N<<1],fa[N<<1][26];
vector<int>v[N<<1];
void init(int x)
{
num=x;
}
void con(int x,int y,int s)
{
int fx=dsu.find(x),fy=dsu.find(y);
if(fx==fy)
{
return;
}
num++;
a[num]=s;
dsu.merge(num,fx);
dsu.merge(num,fy);
v[num].push_back(fx);
v[num].push_back(fy);
}
void dfs(int x,int f)
{
fa[x][0]=f;
for(int i=1;i<=20;i++)
{
fa[x][i]=fa[fa[x][i-1]][i-1];
}
dfn[++cnt]=x;
id[x]=cnt;
for(auto i:v[x])
{
dfs(i,x);
}
R[x]=cnt;
}
}tr1,tr2;
struct seg_tree{
struct Node{
int ls,rs,s;
}node[N*40];
int cnt=0,rt[N<<1];
void pushup(int pos,int ls,int rs)
{
node[pos].s=node[ls].s+node[rs].s;
}
void update(int &pos1,int pos2,int l,int r,int x,int y)
{
if(!pos1)
{
pos1=++cnt;
}
if(l==r)
{
node[pos1].s+=y;
return;
}
int mid=l+r>>1;
if(x<=mid)
{
node[pos1].rs=node[pos2].rs;
update(node[pos1].ls,node[pos2].ls,l,mid,x,y);
}
else
{
node[pos1].ls=node[pos2].ls;
update(node[pos1].rs,node[pos2].rs,mid+1,r,x,y);
}
pushup(pos1,node[pos1].ls,node[pos1].rs);
}
int query(int pos,int l,int r,int ql,int qr)
{
if(l>=ql&&r<=qr)
{
return node[pos].s;
}
int mid=l+r>>1,ans=0;
if(ql<=mid)
{
ans+=query(node[pos].ls,l,mid,ql,qr);
}
if(qr>mid)
{
ans+=query(node[pos].rs,mid+1,r,ql,qr);
}
return ans;
}
}tr;
signed main()
{
n=read(),m=read(),q=read();
for(int i=1;i<=m;i++)
{
e[i]=(edge){read()+1,read()+1};
}
sort(e+1,e+m+1,[&](edge x,edge y)
{
return min(x.st,x.ed)>min(y.st,y.ed);
});
dsu.init(n<<1);
tr1.init(n);
for(int i=1;i<=m;i++)
{
tr1.con(e[i].st,e[i].ed,min(e[i].st,e[i].ed));
}
tr1.dfs(tr1.num,0);
sort(e+1,e+m+1,[&](edge x,edge y)
{
return max(x.st,x.ed)<max(y.st,y.ed);
});
dsu.init(n<<1);
tr2.init(n);
for(int i=1;i<=m;i++)
{
tr2.con(e[i].st,e[i].ed,max(e[i].st,e[i].ed));
}
tr2.dfs(tr2.num,0);
for(int i=1;i<=tr1.cnt;i++)
{
tr.update(tr.rt[i],tr.rt[i-1],1,tr2.cnt,tr2.id[tr1.dfn[i]],tr1.dfn[i]<=n);
}
while(q--)
{
int st=read()+1,ed=read()+1,l=read()+1,r=read()+1;
for(int i=20;i>=0;i--)
{
if(!tr1.fa[st][i]||tr1.a[tr1.fa[st][i]]<l)
{
continue;
}
st=tr1.fa[st][i];
}
for(int i=20;i>=0;i--)
{
if(!tr2.fa[ed][i]||tr2.a[tr2.fa[ed][i]]>r)
{
continue;
}
ed=tr2.fa[ed][i];
}
write(tr.query(tr.rt[tr1.R[st]],1,tr2.cnt,tr2.id[ed],tr2.R[ed])-tr.query(tr.rt[tr1.id[st]-1],1,tr2.cnt,tr2.id[ed],tr2.R[ed])?1:0);
putchar('\n');
}
return 0;
}

浙公网安备 33010602011771号