2025 NOI 做题记录(三)

个人训练赛题解(十五)


\(\text{By DaiRuichen007}\)



Round #69 - 20250409

A. [QOJ5091] 大冬天题

Problem Link

题目大意

给定 \(n,k\),满足 \(n\) 为偶数,给定 \(l_1\sim l_k,r_1\sim r_k\),其中 \(l_i=n-2i+1,r_i=n+2i-1\),求一组完美匹配 \(p\) 使得 \((l_i,r_{p_i})\) 互质的对数尽可能多。

数据范围:\(k\le 10^6\)

思路分析

首先 \(l_i,r_i\) 都是奇数,那么当 \(r_j-l_i\)\(2^t\) 时一定有 \([l,r]\) 互质。

那么考虑最大的 \(t\) 使得 \(l_k+2^t\le r_k\),很显然这样的 \(t\) 满足 \(l_k+2^t\ge r_1\),因为 \([2k,4k-2]\) 中至少有一个 \(2\) 的幂。

那么找到这样的 \(2^t\),设 \(l_k+2^t=r_{m}\),那么还可以匹配 \((l_{k-1},r_{m+1}),\dots,(l_m,r_k)\),从而变成 \(k=m-1\) 的子问题,然后递归即可。

容易发现我们不关心 \(n\) 的值,且此时每对元素都互质。

时间复杂度 \(\mathcal O(k)\)

代码呈现

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int MAXN=1e6+5;
int n,k,a[MAXN];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	string _; cin>>_>>k,n=2*k;
	for(int m=k;m;) {
		LL v=1<<__lg(4*m-2);
		for(LL r=n+2*m-1;m;--m) {
			LL t=n-2*m+1+v;
			if(t>r) break;
			a[(t-n+1)/2]=m;
		}
	}
	cout<<k<<"\n";
	for(int i=1;i<=k;++i) cout<<a[i]<<"\n";
	return 0;
}



B. [CF793E] Problem of offices

Problem Link

题目大意

给定 \(n\) 个点的树,以及 \(4\) 个叶子 \(a,b,c,d\),满足任意两个叶子均在根的不同子树中。

判断是否存在一组树的 dfs 序,使得取出所有叶子 \(e_1\sim e_k\)\((a,b),(c,d)\) 之间的距离都是 \(\dfrac k2\)

数据范围:\(n\le 5000\)

思路分析

首先考虑只有一对点的情况,那么我们可以随意安排根节点除 \(a,b\) 以外的子树以及 \(a,b\) 每个祖先的其他子树。

这些子树可以选择是否放进 \(a,b\) 中间,且每个子树的决策独立,直接 Bitset 优化 dp 即可。

然后我们考虑这四个叶子的相对顺序,显然只可能是 \((a,c,b,d),(a,d,b,c),(b,c,a,d),(b,d,a,c)\) 四种之一。

不妨考虑 \((a,c,b,d)\),那么先考虑 \((a,b)\) 之间的要求,那么 \(c\) 所在子树必须放进中间。

然后是 \(c,d\) 之间的要求,\(b\) 所在子树必须放进中间。

然后我们要合并两侧的方案,很显然只要考虑根节点的其他子树如何排列,这是可以做到的:

  • 一棵子树如果只放进 \(a,b\) 中间,那么 dfs 序放在 \((a,c)\) 中间。
  • 如果只放进 \((c,d)\) 中间,那么 dfs 序放在 \((b,d)\) 中间。
  • 如果同时放进 \(a,b\)\(c,d\) 中间,那么 dfs 序放在 \((c,b)\) 中间。

因此 \((a,c,b),(c,b,d)\) 可以拆成两个独立的子问题,都可以用 Bitset 优化 dp 解决。

时间复杂度 \(\mathcal O\left(\dfrac{n^2}{\omega}\right)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5005;
int n,q[4],h[4],fa[MAXN],sz[MAXN];
vector <int> G[MAXN];
bitset <MAXN> f,dp,o;
bool chk(int l,int r,int d) {
	int w=sz[1]/2-1-sz[h[d]];
	if(w<0) return false;
	dp=f;
	for(int s:{l,r}) for(int x=fa[q[s]],y=q[s];x!=1;y=x,x=fa[x]) {
		for(int u:G[x]) if(u^y) o=dp,o<<=sz[u],dp|=o;
	}
	return dp[w];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>q[0]>>q[1]>>q[2]>>q[3];
	for(int i=2;i<=n;++i) cin>>fa[i],G[fa[i]].push_back(i);
	for(int i=n;i>1;--i) sz[i]+=G[i].empty(),sz[fa[i]]+=sz[i];
	if(sz[1]&1) return cout<<"NO\n",0;
	for(int k:{0,1,2,3}) for(int &u=h[k]=q[k];fa[u]!=1;u=fa[u]);
	f[0]=1;
	for(int i:G[1]) if(i!=h[0]&&i!=h[1]&&i!=h[2]&&i!=h[3]) o=f,o<<=sz[i],f|=o;
	if((chk(0,1,2)||chk(0,1,3))&&(chk(2,3,0)||chk(2,3,1))) cout<<"YES\n";
	else cout<<"NO\n";
	return 0;
}



C. [CF1707E] Replace

Problem Link

题目大意

给定 \(a_1\sim a_n\),定义 \([l,r]\) 进行一次迭代为 \([l,r]\gets f(l,r)=[\min a_{l\sim r},\max a_{l\sim r}]\)\(q\) 次询问 \([l,r]\to [1,n]\) 最少迭代几次。

数据范围:\(n,q\le 10^5\)

思路分析

首先肯定要用倍增维护 \(f^{2^k}(l,r)\),问题是初始的 \((l,r)\) 太多,难以维护,我们需要减少维护的 \([l,r]\) 对数。

可以发现如果两个区间 \([l_1,r_1],[l_2,r_2]\) 有交 \(i\),那么 \(f(l_1,r_1),f(l_2,r_2)\) 有交 \(a_i\)

因此对于两个有交区间 \(X,Y\)\(f(X\cup Y)=f(X)\cup f(Y)\)

那么维护 \(f(l,r)=f(l,l+1)\cup f(l+1,l+2)\cup\cdots\cup f(r-1,r)\)

因此我们维护所有 \(f^{2^k}(i,i+1)\) 即可,转移时设 \([l,r]=f^{2^{k-1}}(i,i+1)\),求 \(\bigcup_{j=l}^{r-1} f^{2^{k-1}}(j,j+1)\),查询也是类似的。

对每层开 ST 表维护即可。

时间复杂度 \(\mathcal O(n\log^2n+q\log n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
typedef array<int,2> seg;
const int MAXN=1e5+5;
seg f[18][18][MAXN];
inline seg operator +(const seg &x,const seg &y) { return {min(x[0],y[0]),max(x[1],y[1])}; }
int bit(int x) { return 1<<x; }
seg qry(int k,int l,int r) {
	int t=__lg(r-l+1);
	return f[k][t][l]+f[k][t][r-bit(t)+1];
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int n,q; cin>>n>>q;
	for(int i=1,x;i<=n;++i) cin>>x,f[0][0][i]={x,x};
	for(int j=1;j<18;++j) for(int i=1;i+bit(j)-1<=n;++i) {
		f[0][j][i]=f[0][j-1][i]+f[0][j-1][i+bit(j-1)];
	}
	for(int k=1;k<18;++k) for(int j=0;j<18;++j) for(int i=1;i+bit(j)-1<=n;++i) {
		f[k][j][i]=qry(k-1,f[k-1][j][i][0],f[k-1][j][i][1]);
	}
	for(int l,r,s;q--;) {
		cin>>l>>r,s=0;
		if(l==1&&r==n) { cout<<"0\n"; continue; }
		for(int k=17;~k;--k) {
			auto i=qry(k,l,r);
			if(1<i[0]||i[1]<n) l=i[0],r=i[1],s+=1<<k;
		}
		auto i=qry(0,l,r);
		if(1<i[0]||i[1]<n) cout<<"-1\n";
		else cout<<s+1<<"\n";
	}
	return 0;
}



D. [CF1483E] Vabank

Problem Link

题目大意

交互器有一个未知变量 \(w\),你每次可以询问一个数 \(x\) 是否 \(\le m\)

初始你有一个权值 \(c=1\),每次询问如果为真则 \(c\gets c+x\),否则 \(c\gets c-x\)

你需要在保证 \(c\) 时刻非负的情况下,在 \(105\) 次询问内求出最优解。

数据范围:\(w\le 10^{14}\)

思路分析

如果直接二分,那么 \(c\) 肯定不够,我们需要询问二分区间左端点 \(l\) 来获取 \(c\),但此时可能耗费太多次数。

首先一个朴素的想法就是先倍增,询问 $20,21,\dots $,最后确定 \(c\) 的一个范围 \([2^x,2^{x+1})\),此时 \(\dfrac rl\le 2\),因此两次询问就能凑够 \(c\)

但此时花费了太多步数,注意到如果 \(w\ge x\) 我们能获得 \(c\),因此不妨把询问的中点调小一点。

这样失败后 \(c\) 减少,但对应的询问区间也变短,否则询问区间也变长。

那么我们需要根据当前的 \(c\) 选取合适的询问中点,首先如果 \(c=0\),且每次都失败。

那么设这种情况下区间长每次变为原来的 \(p\) 倍,则询问次数可以近似为 \(s\log_{1/p}(r-l)\)\(s\) 是一个 \([2,3]\) 间的常数。

在这种情况下花费的 \(c\) 总数 \(c_0\) 可以估计为 \(l\log_{1/p}(r-l)+\dfrac{p}{1-p}(r-l)\),我们可以用 \(c_0\) 近似花费 \(c\) 总量的最大值,因为其他情况的花费应该不会更劣。

那么 \(\dfrac c{c_0}\) 就大致表示 \(c\) 的充裕程度,如果 \(c\) 足够大,那么取二分比例 \(q=0.5\) 最优,否则 \(c=0\) 时必须取 \(q=p\)

不妨用一次函数近似 \(q\),即 \(q=p+\dfrac c{c_0}(0.5-p)\),取 \(p=0.25\) 时可以做到 \(101\) 次询问以内。

时间复杂度 \(\mathcal O(\log w)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll inf=1e14;
ll C;
bool ask(ll z) {
	cout<<"? "<<z<<endl;
	string o; cin>>o;
	if(o=="Lucky!") return C+=z,true;
	else return C-=z,false;
}
const double p=0.25;
void solve() {
	C=1; ll l=0,r=1;
	while(ask(r)) {
		if(r==inf) return cout<<"! "<<r<<endl,void();
		l=r,r=min(r*2,inf);
	}
	while(r>l+1) {
		double z=min(1.,C/(l*(log(r-l)/-log(p))+p/(1-p)*(r-l)));
		ll mid=(p+(0.5-p)*z)*(r-l)+l;
		while(C<mid) ask(l);
		ask(mid)?l=mid:r=mid;
	}
	cout<<"! "<<l<<endl;
}
signed main() {
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*E. [QOJ7793] 雷同

Problem Link

题目大意

给定 \(n\) 个元素 \((w_i,h_i)\),初始 \(h_i=0\),合并两个元素 \((w_i,h_i),(w_j,h_j)\) 的代价为 \(h_i+w_i+w_j\),生成一个新元素 \((w_i+w_j,2\max(h_i,h_j)+1)\)

数据范围:\(n\le 10^4\)

思路分析

把所有元素的合并过程建树,则 \(w\) 的贡献就是 \(w_i\times dep_i\)

然后考虑 \(h\) 的贡献:每个点的贡献是 \(2^{s}-1\),其中 \(s\) 是其轻儿子子树最大深度。

因此 \(h\) 的总贡献就是 \(\sum 2^s-1\),其中 \(s\) 是所有不过根的重链,也可以表示成 \((\sum 2^s)-n+1\),因为共有 \(n\) 条重链。

那么目标就是最小化 \(w_i\times dep_i+\sum 2^s\)

不妨钦定 \(dep\) 数组,然后求最小点 \(\sum 2^s\)

自下往上合并,假设当前处理到 \(dep=d\) 的点,每个点当前的深度为 \(h_0\sim h_q\)

\(h\) 单调递减,则匹配 \((h_0,h_1)\) 肯定是最优的,否则 \(h_1\) 向上传递,产生一个 \(h\) 更大的点,且代价至少为 \(2^{h_1+1}\),显著不优。

因此最优策略就是把 \(h_{2i},h_{2i+1}\) 全部合并。

很显然深度更深的点 \(w\) 一定更小,因此按 \(w\) 排序,那么按深度降序处理等价于处理 \(w\) 的一个前缀。

假设加入 \(w_k\) 的时候当前层已经有 \(t\) 个叶子,则 \(k\) 所在长链长度就是 \(t\) 的二进制表示中最低的 \(1\),则贡献为 \(\mathrm{lowbit}(t)\)

那么 \(f_{k,i}\) 表示 \([1,k-1]\) 插入后当前层有 \(t\) 个叶子,转移为:

  • 加入叶子:\(f_{k-1,i}\to f_{k,i+1}+\mathrm{lowbit}(i)\)
  • 向上合并一层:\(f_{k,2x}\to f_{k,x}+\sum_{j\le k} w_j\)

直接 dp 即可。

时间复杂度 \(\mathcal O(n^2)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=10005;
const ll inf=1e18;
int n;
ll w[MAXN],f[MAXN];
void solve() {
	cin>>n;
	for(int i=1;i<=n;++i) cin>>w[i];
	sort(w+1,w+n+1);
	for(int i=1;i<=n;++i) w[i]+=w[i-1];
	for(int i=0;i<=n;++i) f[i]=i>1?inf:0;
	for(int d=1;d<=n;++d) {
		for(int i=n-1;~i;--i) f[i+1]=f[i]+(i&-i),f[i]=inf;
		for(int i=n;~i;--i) if(~i&1) f[i/2]=min(f[i/2],f[i]+w[d]);
	}
	cout<<f[1]-n+1<<"\n";
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



*F. [QOJ7837] 挑战积和式

Problem Link

题目大意

给定 \(a_1\sim a_n\),选择 \(k\) 个元素 \(b_1\sim b_k\) 使得 \(\sum a_{b_i}-\prod b_i\) 最大。

数据范围:\(k,n\le 10^6,a_i\le 10^9\)

思路分析

首先我们设当前 \(\sum a=A,\prod b=B\),则加入一个新元素 \(x\) 收益为 \(a_x-(x-1)B\),容易发现 \(B>a_x\)\(x>1\) 一定不优。

因此 \(x>1\) 的元素只有 \(\mathcal O(\log V)\) 个,只要考虑 \(k\le \log V\) 的情况。

此时可以设计状态 \(f_{i,B}\)\(i\) 个元素,\(\prod b=B\) 的最大 \(\sum a\)

转移是 \(f_{i,x}+f_{j,y}\to f_{i+j,x\times y}\),复杂度 \(\mathcal O(V\log V)\)

\(V\) 太大无法接受,可以考虑分治:即把所有元素分成两部分,使得每部分的 \(\prod V\) 都较小。

可以证明对于若干 \(\le \dfrac 23\) 且总和 \(=1\) 的元素,可以分成大小 \(\le \dfrac 23\) 的两部分:

如果最大值 \(\ge \dfrac 12\) 则单独分组,剩下的元素分一组。

否则元素个数显然 \(\ge 3\),最大值 \(\ge \dfrac 13\),取出最小的两个元素 \(x,y\),则 \(x+y\le \dfrac 23\),用 \(x+y\) 替换 \(x,y\),然后归纳即得证。

\(w=V^{2/3}\),由于 \(n\le w\),那么我们可以把所有元素分成乘积 \(\le w\) 的两部分。

因此只需要处理 \(B\le w\) 的情况,复杂度变为 \(\mathcal O(w\log w\log V)\)

最后我们要合并所有 \(f_i,f_{k-i}\),即求 \(\max f_{i,x}+f_{k-i,y}-xy\),可以斜率优化解决。

但这不足以通过,我们尝试减少计算一些不必要的 \(f_i\),进一步观察:

对于最大值 \(\le \dfrac 13\) 且总和 \(=1\) 的若干元素,可以把他们等分成两部分,且每部分总和 \(\le\dfrac 23\)

假设所有元素从小到大排序后为 \(x_1\sim x_{n}\)\(2\mid n\)(否则加入一个 \(x_1=0\) 即可)。

那么选出 \(x_2,x_4\dots ,x_n\),首先 \(x_2+x_4+\cdots +x_n\ge x_1+x_3+\cdots+x_{n-1}\)

又因为 \(x_2+x_4+\cdots +x_{n-2}\le x_1+x_3+x_5+\cdots +x_{n-1}\),因此 \(x_2+x_4+\cdots +x_{n-2}\le \dfrac 12(1-x_n)\)

那么 \(x_2+x_4+\cdots +x_n\le \dfrac {1+x_n}2\le \dfrac 23\),证毕。

因此所有元素 \(\le V^{1/3}\) 时只需要求出 \(f_{k/2}\),答案一定为 \(f_{k/2,x}+f_{k/2,y}-xy\)

如果最大值 \(>\dfrac 13\),那么可以分成 \(f_1\)\(f_{k-1}\) 合并。

因此用快速幂优化,只要计算 \(\mathcal O(\log\log V)\)\(f_i\),可以接受。

时间复杂度 \(\mathcal O(w\log w\log\log V)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e6+5,V=1e6;
int n,m,M,q[MAXN],st[MAXN];
ll f[32][MAXN],ans=0;
bool e[32];
void mul(ll *a,ll *b,ll *c) {
	for(int i=1;i<=V;++i) for(int j=1;i*j<=V;++j) c[i*j]=max(c[i*j],a[i]+b[j]);
}
void dp(ll *a,ll *b) {
	int t=0;
	for(int i=1;i<=V;++i) {
		while(t>1&&(a[q[t]]-a[q[t-1]])*(i-q[t])<(a[i]-a[q[t]])*(q[t]-q[t-1])) --t;
		q[++t]=i;
	}
	for(int i=V,j=1;i;--i) {
		while(j<t&&1ll*i*(q[j+1]-q[j])<(a[q[j+1]]-a[q[j]])) ++j;
		ans=max(ans,a[q[j]]+b[i]-1ll*i*q[j]);
	}
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>M,m=min(M,30),e[0]=e[1]=true;
	for(int i=1;i<=n;++i) cin>>f[1][i];
	for(int i=m-1;i>1;i>>=1) e[i]=1;
	for(int i=m/2;i>1;i>>=1) e[i]=1;
	for(int i=m/2+1;i>1;i>>=1) e[i]=1;
	for(int i=2;i<m;++i) if(e[i]) {
		int j=i/2;
		mul(f[j],f[j],f[j*2]),e[j*2]=true;
		if(i&1) mul(f[j*2],f[1],f[i]);
	}
	dp(f[1],f[m-1]),dp(f[m/2],f[(m+1)/2]);
	cout<<ans+f[1][1]*(M-m)<<"\n";
	return 0;
}



Round #70 - 20250410

A. [QOJ4212] Brackets

Problem Link

题目大意

给定长度为 \(2n\) 的字符串,把所有位置两两匹配,匹配的位置上要填相同的字符,构造一个满足要求的合法括号串。

数据范围:\(n\le 2\times 10^5\)

思路分析

一个朴素的想法就是选最靠前的 \(n/2\) 对匹配填左括号。

但这样可能导致后缀的左括号太多,因此我们每次需要检验后缀的左括号个数:注意到一个字符串合法当且仅当第 \(i\) 个括号位置 \(\le 2i-1\),因此把每个左括号和最接近的奇数下标匹配,如果可以匹配就贪心加入。

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=4e5+5;
int n,a[MAXN],b[MAXN],t[MAXN];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n;
	if(n&1) return cout<<"(\n",0;
	n*=2;
	for(int i=1,c;i<=n;++i) cin>>c,b[c]?a[b[c]]=i:b[c]=i;
	set <int> S;
	for(int i=1;i<n;i+=2) S.insert(i);
	for(int i=1;i<=n;++i) if(a[i]&&S.size()) {
		if(*S.begin()<i) return cout<<"(\n",0;
		auto it=S.lower_bound(a[i]);
		if(it!=S.end()) S.erase(it),S.erase(S.begin()),t[i]=t[a[i]]=1;
	}
	if(S.size()) return cout<<"(\n",0;
	for(int i=1;i<=n;++i) cout<<")("[t[i]];
	return 0;
}



B. [QOJ4213] Circles

Problem Link

题目大意

定义一个数组 \(b_0\sim b_{m-1}\) 的权值为:最大的 \(\sum x_i\),使得 \(x_i+x_{(i+1)\bmod m}\le b_i\)

给定 \(a_1\sim a_n\),对每个前缀求其权值。

数据范围:\(n\le 10^5\)

思路分析

考虑线性规划对偶:那么我们得到 \(\min \sum b_iy_i\) 使得 \(y_i+y_{(i-1)\bmod m}\ge 1\)

可以证明每个 \(y_i\in \{0,0.5,1\}\),那么直接 dp,\(f_{i,c,d}\) 表示 \(y_1=c,y_i=d\) 的最小花费,求答案的时候把 \(c,d\) 拼起来即可。

时间复杂度 \(\mathcal O(n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
int n,a[MAXN]; ll f[MAXN][3][3];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0) ;
	cin>>n;
	for(int i=1;i<=n;++i) cin>>a[i];
	memset(f,0x3f,sizeof(f));
	for(int c:{0,1,2}) f[1][c][c]=a[1]*c;
	for(int i=2;i<=n;++i) for(int x:{0,1,2}) for(int y:{0,1,2}) for(int z:{0,1,2}) if(y+z>=2) {
		f[i][x][z]=min(f[i][x][z],f[i-1][x][y]+z*a[i]);
	}
	for(int i=3;i<=n;++i) {
		ll ans=1e18;
		for(int x:{0,1,2}) for(int y:{0,1,2}) if(x+y>=2) ans=min(ans,f[i][x][y]);
		cout<<ans/2<<"."<<"05"[ans&1]<<" \n"[i==n];
	}
	return 0;
}



C. [CF1710D] Recover the Tree

Problem Link

题目大意

你需要构造一棵 \(n\) 个点的树,使得对于每个子区间 \([l,r]\),这些点在树上的连通性和输入的 \(a_{l,r}\) 一致。

数据范围:\(n\le 2000\),保证有解。

思路分析

考虑逐步加点,那么加入 \([1,n-1]\) 后会形成一个森林。

那么考虑所有 \(a_{x,n}=1\) 的区间 \([x,n]\),按 \(x\) 从大到小依次加入即可。

首先 \([x,n-1]\) 的点构成若干个连通块,我们要求将他们连通,且任何一个子区间都不连通。

如果 \([x,n-1]\) 中只有一个点,那么直接和 \(n\) 相连,否则设这些连通块排序后为 \(b_1\sim b_q\)

那么 \(q=2\) 时无解,否则 \(b_1,b_2\)\(n\) 相连,\(b_{3}\sim b_q\)\(b_1\) 相连即可。

时间复杂度 \(\mathcal O(n^2)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
int n,p[MAXN];
char f[MAXN][MAXN];
void solve() {
	cin>>n,iota(p+1,p+n+1,1);
	for(int i=1;i<=n;++i) for(int j=i;j<=n;++j) cin>>f[i][j];
	for(int r=1;r<=n;++r) for(int l=r-1;l;--l) if(f[l][r]=='1'&&l<p[r]) {
		cout<<l<<" "<<r<<"\n";
		if(p[p[r]-1]>l) {
			cout<<p[r]-1<<" "<<l<<"\n";
			for(int i=p[p[r]-1]-1;p[i]>l;i=p[i]-1) cout<<i<<" "<<r<<"\n";
		}
		for(int i=r;i>l;--i) p[i]=p[l];
	}
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int _; cin>>_;
	while(_--) solve();
	return 0;
}



D. [QOJ7979] 棋盘

Problem Link

题目大意

在无穷大二维平面上选出 \(\le X\) 个格子,必须包含 \((1,1)\),定义每个格子的权值为从 \((1,1)\) 仅通过选出格子走到当前格子的方案数。

多次询问 \(n\),要求可以把 \(n\) 表示成 \(\le Y\) 个不同的格子的权值之和。

数据范围:\(n\le 10^{100},X=960,Y=240\)

思路分析

首先朴素想法就是在二进制下解决:构造权值为 \(2^1,2^2,\dots\) 的格子,一种方法为:

1 1
1 2 2
  2 4 4
    4 8

这样花费 \(X=3\log_2n=996,Y=\log_2 n=332\),需要优化。

然后可以想到换进制,例如三进制和六进制,可以得到不同的做法,但都无法通过本题。

一个想法是使用一些非平凡的进制,例如斐波那契进制:

1 1 1
1 2 3 3
  2 5 8  8
    5 13 21

可以发现这样能构造出所有的斐波那契数,记 \(\varphi=\dfrac{\sqrt 5+1}2\)

\(X=2\log_{\varphi}n\),由于 \(\mathrm{Fib}_{480}>10^{100}\),因此 \(X\le 960\)

然后考虑斐波那契进制下的分解,容易发现分解形式中没有两个相邻的 \(1\),否则可以进位,那么 \(Y=\dfrac 12\log_{\varphi}n\),即 \(Y\le 240\),满足题意。

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int k=480;
struct bi {
	static const int B=1e8;
	int a[16];
	bi() { memset(a,0,sizeof(a)); }
	inline friend bi operator +(const bi &u,const bi &v) {
		bi w;
		for(int i=0;i<15;++i) {
			w.a[i]+=u.a[i]+v.a[i];
			if(w.a[i]>=B) ++w.a[i+1],w.a[i]-=B;
		}
		return w;
	}
	inline friend bi operator -(const bi &u,const bi &v) {
		bi w;
		for(int i=0;i<15;++i) {
			w.a[i]+=u.a[i]-v.a[i];
			if(w.a[i]<0) --w.a[i+1],w.a[i]+=B;
		}
		return w;
	}
	inline friend bool operator >=(const bi &u,const bi &v) {
		for(int i=15;~i;--i) if(u.a[i]!=v.a[i]) return u.a[i]>v.a[i];
		return true;
	}
	void read() {
		memset(a,0,sizeof(a));
		string _; cin>>_;
		for(int i=0;_.size();++i) {
			int w=_.size(),t=min(8,w);
			for(int j=t;j;--j) a[i]=a[i]*10+_[w-j]-'0';
			_.resize(w-t);
		}
	}
	
}	f[k+5];
int K,q,X,Y,g[k+5];
signed main() {
	cin>>K>>q>>X>>Y;
	f[0].a[0]=f[1].a[0]=1;
	for(int i=2;i<k;++i) f[i]=f[i-1]+f[i-2];
	vector <array<int,2>> a;
	a.push_back({1,1}),a.push_back({1,2}),g[0]=0,g[1]=1;
	for(int i=1;i<k/2;++i) {
		a.push_back({i,i+2});
		a.push_back({i+1,i});
		a.push_back({i+1,i+1}),g[2*i]=a.size()-1;
		a.push_back({i+1,i+2}),g[2*i+1]=a.size()-1;
	}
	int n=a.size(); cout<<n<<"\n";
	for(auto o:a) cout<<o[0]<<" "<<o[1]<<"\n";
	for(bi w;q--;) {
		w.read(); string z(n,'0');
		for(int i=k-1;~i;--i) if(w>=f[i]) w=w-f[i],z[g[i]]='1';
		cout<<z<<"\n";
	}
	return 0;
}



*E. [CF1630F] Making It Bipartite

Problem Link

题目大意

给定 \(n\) 个点,有权值 \(a_1\sim a_n\),如果 \(a_i\mid a_j\) 则连边 \((i,j)\),删除最少的点使得图是二分图。

数据范围:\(n,a_i\le 5\times 10^4,\)

思路分析

我们如果 \(a_i\mid a_j\mid a_k\) 就构成三元环 \((i,j,k)\),因此我们要求所有点要么只能是约数,要么只能是倍数。

\(i_0,i_1\) 表示一个点只是约数或只是倍数。

那么如果 \(a_i\mid a_j\),则仅有 \((i_0,j_1)\) 合法。

在不合法的状态之间连边:\(i_1\to i_0,i_0\to j_0,i_1\to j_0,i_1\to j_1\),容易证明该图是闭合 DAG。

我们发现一组合法方案对应该图上的一条反链,所求即为最长反链,Dilworth 定理转成求最小链覆盖即可。

时间复杂度 \(\mathcal O((n\log V)^{1.5})\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e4+5,inf=1e9;
namespace F {
const int MAXV=2e5+5,MAXE=5e6+5,inf=1e9;
struct Edge {
	int v,f,lst;
}	G[MAXE];
int S,T,tot=1,hd[MAXV],cur[MAXV],dep[MAXV];
inline void init() { tot=1,memset(hd,0,(T+1)<<2); }
inline void adde(int u,int v,int w) { G[++tot]={v,w,hd[u]},hd[u]=tot; }
inline void link(int u,int v,int w) { adde(u,v,w),adde(v,u,0); }
inline bool BFS() {
	memcpy(cur,hd,(T+1)<<2),memset(dep,-1,(T+1)<<2);
	queue <int> Q;
	Q.push(S),dep[S]=0;
	while(!Q.empty()) {
		int u=Q.front(); Q.pop();
		for(int i=hd[u];i;i=G[i].lst) if(G[i].f&&dep[G[i].v]==-1) {
			dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
		}
	}
	return ~dep[T];
}
inline int dfs(int u,int f) {
	if(u==T) return f;
	int r=f;
	for(int i=cur[u];i;i=G[i].lst) {
		int v=G[cur[u]=i].v;
		if(G[i].f&&dep[v]==dep[u]+1) {
			int g=dfs(v,min(r,G[i].f));
			if(!g) dep[v]=-1;
			G[i].f-=g,G[i^1].f+=g,r-=g;
		}
		if(!r) return f;
	}
	return f-r;
}
inline int Dinic() {
	int f=0;
	while(BFS()) f+=dfs(S,inf);
	return f;
}
}
int a[MAXN],id[MAXN];
inline void solve() {
	int n;
	scanf("%d",&n),F::init();
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),id[a[i]]=i;
	int S=F::S=4*n+1,T=F::T=4*n+2;
	for(int i=1;i<=n;++i) {
		F::link(S,i,1),F::link(S,i+n,1);
		F::link(i+2*n,T,1),F::link(i+3*n,T,1);
		F::link(i+n,i+2*n,1); 
		for(int v=2*a[i];v<MAXN;v+=a[i]) if(id[v]) {
			F::link(i,id[v]+2*n,1);
			F::link(i+n,id[v]+2*n,1);
			F::link(i+n,id[v]+3*n,1);			
		}
	}
	printf("%d\n",n-(2*n-F::Dinic()));
	for(int i=1;i<=n;++i) id[a[i]]=0;
}
signed main() {
	int T;
	scanf("%d",&T);
	while(T--) solve();
}



*F. [QOJ7650] 没有创意的题目名称

Problem Link

题目大意

给定 \(a_0\sim a_n\),求有多少 \(f_0\sim f_n\) 满足 \(f_i\in[0,a_i]\),且 \(\forall i+j\le n,f_{i+j}=f_{f_i+f_j}\)

数据范围:\(n\le 2000\)

思路分析

首先打表发现合法序列一定形如:前缀为 \(f_i=i\),从某个时刻开始产生循环节。

可以严谨证明:找到首个出现 \(f_i=f_j\) 的位置 \((i,j)\),则 \(f_{i+1}=f_{f_i+f_1}=f_{f_j+f_1}=f_{j+1}\),因此后面就会产生循环节。

先假设 \(f_0=0\),此时 \(f_i=f_{f_i+f_0}=f_{f_i}\),那么枚举第一个 \(f_i\ne i\) 的位置 \(p\),则 \(f_0\sim f_{p-1}=[0,1,\dots,p-1]\)

考虑 \(f_p\) 的取值,此时 \(f_{f_p}=f_p\),因此循环节 \(k\mid f_p-p\),然后 $f_{p+1}=f_{f_p+1},\dots $ 推出所有 \(f_i-i\) 都是 \(k\) 的倍数。

那么 \(f_i=f_{i\bmod k}\),则 \(f_{f_i+f_j}=f_{f_i+f_j\bmod k}=f_{i+j\bmod k}=f_{i+j}\),大部分情况下该序列已经合法。

我们还有一个要求:如果 \(i+j\le n\),则 \(f_i+f_j\le n\):取 \(i=j\) 得到 \(i\le n/2\)\(f_i\le n/2\),充分性显然。

因此枚举 \(k,p\),那么 \([p,p+k)\) 中的元素 \(f_j\) 可以填 \(\le \min(n/2,\min a_{j+tk})\),且 \(=j+rk\) 的元素,如果 \(j>\dfrac n2\),则 \(f_j=j\)

动态维护区间乘积即可。

然后是 \(f_0\ne 0\) 的情况,此时 \(f_{f_0+f_0}=f_0\),因此循环节 \(k\mid 2f_0\),那么 \(f_0\bmod k\in\{0,k/2\}\)

如果 \(f_0\bmod k=0\),那么所有 \(f_i\bmod k=i\),否则 \(f_i\bmod k=i+\dfrac k2\),枚举 \(k\) 后暴力算每个 \(f\) 的取值即可。

时间复杂度 \(\mathcal O(n^2)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2005,MOD=998244353;
int n,m,a[MAXN],f[MAXN];
ll c[MAXN],inv[MAXN],ans=0;
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n,inv[1]=1;
	for(int i=2;i<=n;++i) inv[i]=inv[MOD%i]*(MOD-MOD/i)%MOD;
	for(int i=0;i<=n;++i) cin>>a[i];
	while(m<=a[m]) ++m;
	if(m>n) ++ans;
	for(int k=1;k<=n;++k) {
		for(int i=n;i>=1;--i) f[i]=i+k>n?a[i]:min(a[i],f[i+k]);
		c[0]=1;
		for(int i=1;i<=n;++i) {
			if(f[i]<i) c[i]=0;
			else if(i>n/2) c[i]=1;
			else c[i]=(min(f[i],n/2)-i)/k+1;
		}
		ll p=1;
		for(int i=n,c0=0;~i;--i) {
			c[i]?p=p*c[i]%MOD:++c0;
			if(i+k>n) continue;
			c[i+k]?p=p*inv[c[i+k]]%MOD:--c0;
			if(i<=m&&!c0) ans=(ans+p)%MOD;
		}
	}
	for(int k=1;k*2<=n;++k) {
		ll p=1;
		for(int i=0;i<k;++i) {
			int up=n/2;
			for(int j=i;j<=n;j+=k) up=min(up,a[j]);
			if(up<i) { p=0; break; }
			p=p*((up-i)/k+(i>0))%MOD;
		}
		ans=(ans+p)%MOD;
	}
	for(int k=2;k<=n;k+=2) {
		ll p=1;
		for(int i=0;i<k;++i) {
			int up=n/2;
			for(int j=i;j<=n;j+=k) up=min(up,a[j]);
			int lo=i<k/2?i+k/2:i-k/2;
			if(up<lo) { p=0; break; }
			p=p*((up-lo)/k+1)%MOD;
		}
		ans=(ans+p)%MOD;
	}
	cout<<ans<<"\n";
	return 0;
}



Round #71 - 20250416

A. [UOJ181] 密码锁

Problem Link

题目大意

给定 \(n\) 个点的完全图,每个点会随机定向,有 $m $ 条边定向概率给定,其他边两种方向的概率均为 \(\dfrac 12\),求期望强连通分量个数。

数据范围:\(n\le 38,m\le 19\)

思路分析

首先竞赛图强连通分量个数等价于计算 \((S,T)\) 割的个数,使得不存在 \(T\to S\) 的边。

那么枚举 \(S\) 可以做到 \(\mathcal O(2^n)\)

我们要算 \(S\to T\) 每条边的边权积,注意到大部分边的权值都是 \(\dfrac 12\),因此先乘上 \(2^{-|S|\times |T|}\),然后每条特殊便乘上一个权值即可。

那么这样我们就只关心这些 \(S,T\) 之间的特殊边了。

由于特殊边数量很少,因此可以对于每个特殊边构成的连通块,其大小 \(\le m+1\),在其中枚举 \(S\) 的复杂度是可以接受的,然后用背包记录每种 \(|S|\) 对应的边权和即可。

时间复杂度 \(\mathcal O(n2^m)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MOD=998244353,i2=(MOD+1)/2,Q=10000;
ll ksm(ll a,ll b=MOD-2) { ll s=1; for(;b;a=a*a%MOD,b>>=1) if(b&1) s=s*a%MOD; return s; }
int n,m,dsu[45],u[45],v[45],rk[45];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
vector <int> p[45];
ll f[45],g[45],w[45],ans;
signed main() {
	ios::sync_with_stdio(false);
	cin>>n>>m,iota(dsu+1,dsu+n+1,1);
	for(int i=1;i<=m;++i) cin>>u[i]>>v[i]>>w[i],w[i]=w[i]*ksm(Q)%MOD,dsu[find(u[i])]=find(v[i]);
	for(int i=1;i<=n;++i) p[find(i)].push_back(i);
	f[0]=1;
	for(int i=1;i<=n;++i) if(p[i].size()) {
		int k=p[i].size();
		memset(g,0,sizeof(g)),memset(rk,-1,sizeof(rk));
		for(int j=0;j<k;++j) rk[p[i][j]]=j;
		for(int s=0;s<(1<<k);++s) {
			ll z=1; int c=__builtin_popcount(s);
			for(int e=1;e<=m;++e) if(~rk[u[e]]&&~rk[v[e]]&&(s>>rk[u[e]]&1)!=(s>>rk[v[e]]&1)) {
				z=z*2*(s>>rk[u[e]]&1?w[e]:1+MOD-w[e])%MOD;
			}
			for(int j=c;j<=n;++j) g[j]=(g[j]+f[j-c]*z)%MOD;
		}
		memcpy(f,g,sizeof(f));
	}
	for(int i=1;i<n;++i) ans=(ans+f[i]*ksm(i2,i*(n-i)))%MOD;
	ans=(ans+1)*ksm(Q,n*(n-1))%MOD;
	printf("%lld\n",ans);
	return 0;
}



B. [P6782] rplexq

Problem Link

题目大意

给定 \(n\) 个点的树,\(m\) 次询问 \((l,r,x)\) 有多少 \(l\le i<j\le r\) 满足 \(\mathrm{LCA}(i,j)=x\)

数据范围:\(n,m\le 2\times 10^5\)

思路分析

要算的就是 \(x\) 子树有多少 \([l,r]\) 中的点,以及 \(x\) 每个儿子的子树有多少 \([l,r]\) 中的点。

朴素想法就是按 dfn 序拆成区间,再差分成前缀,相当于动态加点,查询 \([l,r]\) 中节点个数。

如果暴力对每个儿子查询答案,\(\deg_x\) 较大时无法处理,因此可以根号分治。

对于 \(\deg_x\le B\) 的询问,Sqrt-Tree 做到 \(\mathcal O(1)\) 查询即可。

然后对于 \(\deg_x>B\) 的询问,按 \(x\) 分别处理。

\(x\) 子树的所有点按编号排序,染色为属于哪个儿子的子树,询问就是区间有多少同色点对。

可以用莫队维护,复杂度 \(n\sqrt m\),但 \(\sum n\) 依然无法接受。

可以想到对这样的 \(x\)\(siz\)\(B\) 大子树用刚才的 Sqrt-Tree 处理,这样 \(\sum n\) 就可以接受了。

注意离线的时候要做到线性空间。

时间复杂度应该不会超过 \(\mathcal O((n+m)\sqrt n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,B=100;
int n,m,rt,ql[MAXN],qr[MAXN],lst[MAXN],pre[MAXN],ed[MAXN];
ll ans[MAXN];
basic_string <int> G[MAXN],Q[MAXN],at[MAXN];
int siz[MAXN],dfn[MAXN],efn[MAXN],dcnt,rk[MAXN];
struct SqrtTree {
	int s1[MAXN],s2[MAXN],s3[MAXN];
	void add(int x,int y) {
		for(int i=((x>>7)<<7);i<=x;++i) s1[i]+=y;
		for(int i=((x>>14)<<7);i<(x>>7);++i) s2[i]+=y;
		for(int i=0;i<(x>>14);++i) s3[i]+=y;
	}
	int qry(int x) { return s1[x]+s2[x>>7]+s3[x>>14]; }
	int qry(int l,int r) { return qry(l)-qry(r+1); }
}	T;
void dfs1(int u,int fz) {
	siz[u]=1;
	for(int v:G[u]) if(v^fz) dfs1(v,u),siz[u]+=siz[v];
	if(fz) G[u].erase(find(G[u].begin(),G[u].end(),fz));
	sort(G[u].begin(),G[u].end(),[&](int x,int y){ return siz[x]>siz[y]; });
}
void dfs2(int u) {
	dfn[u]=++dcnt,rk[dcnt]=u;
	for(int v:G[u]) if(v) dfs2(v);
	efn[u]=dcnt;
}
bitset <MAXN> vis;
int a[MAXN],tp,cl[MAXN],ct[MAXN];
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>rt;
	for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	for(int i=1,x;i<=m;++i) cin>>ql[i]>>qr[i]>>x,Q[x].push_back(i);
	dfs1(rt,0),dfs2(rt);
	for(int u=1;u<=n;++u) {
		at[dfn[u]-1].push_back(u);
		at[dfn[u]].push_back(u);
		if((int)G[u].size()<=B) {
			for(int v:G[u]) at[efn[v]].push_back(u);
		} else {
			for(int i=0;i<B;++i) at[efn[G[u][i]]].push_back(u);
			at[efn[u]].push_back(u);
		}
	}
	for(int i=1;i<=n;++i) {
		T.add(rk[i],1);
		for(int u:at[i]) for(int x:Q[u]) {
			int c=T.qry(ql[x],qr[x])-lst[x];
			if(i>=dfn[u]) ans[x]+=1ll*pre[x]*c,pre[x]+=c;
			lst[x]+=c,ed[x]=c;
		}
	}
	for(int u=1;u<=n;++u) if((int)G[u].size()>B+1) {
		for(int x:Q[u]) ans[x]+=1ll*ed[x]*(ed[x]-1)/2;
		for(int i=B;i<(int)G[u].size();++i) {
			for(int v=G[u][i],j=dfn[v];j<=efn[v];++j) cl[rk[j]]=i-B,vis.set(rk[j]);
		}
		for(int i=vis._Find_first();i<=n;i=vis._Find_next(i)) a[++tp]=i;
		vector <int> op;
		for(int x:Q[u]) {
			ql[x]=lower_bound(a+1,a+tp+1,ql[x])-a;
			qr[x]=upper_bound(a+1,a+tp+1,qr[x])-a-1;
			if(ql[x]<=qr[x]) op.push_back(x);
		}
		int D=tp/sqrt(op.size()+1)+1;
		sort(op.begin(),op.end(),[&](int x,int y){
			int dx=(ql[x]-1)/D,dy=(ql[y]-1)/D;
			if(dx^dy) return dx<dy;
			return dx&1?qr[x]>qr[y]:qr[x]<qr[y];
		});
		int L=1,R=0; ll tot=0;
		auto add=[&](int x) { tot+=ct[cl[x]]++; };
		auto del=[&](int x) { tot-=--ct[cl[x]]; };
		for(int x:op) {
			while(L>ql[x]) add(a[--L]);
			while(R<qr[x]) add(a[++R]);
			while(L<ql[x]) del(a[L++]);
			while(R>qr[x]) del(a[R--]);
			ans[x]-=tot;
		}
		tp=0,memset(ct,0,(G[u].size()-B)<<2),vis.reset();
	}
	for(int i=1;i<=m;++i) cout<<ans[i]<<"\n";
	return 0;
}



C. [CF1163F] Indecisive Taxi Fee

Problem Link

题目大意

给定 \(n\) 个点 \(m\) 条边的无向图,\(q\) 次询问修改一条边权值后 \(1\to n\) 的最短路(询问之间独立)。

数据范围:\(n,m,q\le 2\times 10^5\)

思路分析

首先修改的边不在最短路上是平凡的,核心就是对于某条最短路上的边 \(e\),求出不经过 \(e\) 的最短路。

那么我们可以证明对于每个 \(e\),其最优策略都是找到一条不在最短路上的边 \(f=(u,v)\),然后沿最短路走 \(1\to u,v\to n\)

因此对于每个不在最短路上的点,求出 \(1\to u,1\to n\) 最短路的 lcp 以及 \(v\to n,1\to n\) 最短路的 lcs。

那么每条边会更新 \(1\to n\) 最短路序列上一个区间的答案,用数据结构维护之即可。

时间复杂度 \(\mathcal O(m\log m+q)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,m,q,a[MAXN],b[MAXN],c[MAXN];
struct Edge { int v,w,id; };
vector <Edge> G[MAXN];
ll ds[MAXN],dt[MAXN];
bool vis[MAXN];
void dijk(int S,ll *d) {
	memset(d,0x3f,sizeof(ds));
	memset(vis,0,sizeof(vis));
	priority_queue <array<ll,2>,vector<array<ll,2>>,greater<array<ll,2>>> Q;
	Q.push({d[S]=0,S});
	while(Q.size()) {
		int u=Q.top()[1]; Q.pop();
		if(vis[u]) continue; vis[u]=true;
		for(auto e:G[u]) if(d[e.v]>d[u]+e.w) {
			Q.push({d[e.v]=d[u]+e.w,e.v});
		}
	}
}
int st[MAXN],tp,L[MAXN],R[MAXN],rk[MAXN];
const int N=1<<18;
ll tr[N<<1];
void upd(int l,int r,ll z) {
	for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1) {
		if(~l&1) tr[l^1]=min(tr[l^1],z);
		if(r&1) tr[r^1]=min(tr[r^1],z);
	}
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m>>q;
	for(int i=1;i<=m;++i) {
		cin>>a[i]>>b[i]>>c[i];
		G[a[i]].push_back({b[i],c[i],i});
		G[b[i]].push_back({a[i],c[i],i});
	}
	dijk(1,ds),dijk(n,dt);
	st[++tp]=1;
	for(int u=1;u!=n;) {
		for(auto e:G[u]) if(dt[e.v]+e.w==dt[u]) {
			rk[e.id]=tp,u=e.v; break;
		}
		st[++tp]=u;
	}
	memset(L,0x3f,sizeof(L));
	for(int i=1;i<=tp;++i) L[st[i]]=i,R[st[i]]=i-1;
	vector <int> ord;
	for(int i=1;i<=n;++i) ord.push_back(i);
	sort(ord.begin(),ord.end(),[&](int x,int y){ return ds[x]<ds[y]; });
	for(int u:ord) for(auto e:G[u]) if(!rk[e.id]&&ds[e.v]==ds[u]+e.w) L[e.v]=min(L[e.v],L[u]);
	sort(ord.begin(),ord.end(),[&](int x,int y){ return dt[x]<dt[y]; });
	for(int u:ord) for(auto e:G[u]) if(!rk[e.id]&&dt[e.v]==dt[u]+e.w) R[e.v]=max(R[e.v],R[u]);
	memset(tr,0x3f,sizeof(tr));
	for(int i=1;i<=m;++i) if(!rk[i]) {
		if(L[a[i]]<=R[b[i]]) upd(L[a[i]],R[b[i]],ds[a[i]]+c[i]+dt[b[i]]);
		if(L[b[i]]<=R[a[i]]) upd(L[b[i]],R[a[i]],ds[b[i]]+c[i]+dt[a[i]]);
	}
	for(int i=1;i<N;++i) tr[i<<1]=min(tr[i<<1],tr[i]),tr[i<<1|1]=min(tr[i<<1|1],tr[i]);
	for(int e,x;q--;) {
		cin>>e>>x;
		if(rk[e]) cout<<min(tr[rk[e]+N],ds[n]-c[e]+x)<<"\n";
		else cout<<min({ds[n],ds[a[e]]+x+dt[b[e]],ds[b[e]]+x+dt[a[e]]})<<"\n";
	}
	return 0;
}



D. [CF1458D] Flip and Reverse

Problem Link

题目大意

给定 01 串 \(S\),每次可以选择 \(S\) 中一个 \(0,1\) 数量相等的子串取反后翻转,求能得到的字典序最小的 \(S\)

数据范围:\(|S|\le 5\times 10^5\)

思路分析

考虑 \(\texttt {‘0’}\to -1,\texttt {‘1’}\to +1\),设翻转了 \(S[l,r]\),观察其前缀和数组 \(d_i\) 的变化。

那么由于 \(S[l,r]\)\(0,1\) 数量相等,因此 \(d_{l-1}=d_r\),观察发现新的 \(d_i\) 就是原先的 \(d_{r+l-1-i}\)

这等价于翻转 \(d[l,r-1]\)

如果我们把所有 \(d_i\) 看成节点,在所有 \(d_i\to d_{i+1}\) 之间连有向边。

那么 \(S\) 就是这张图上的一个欧拉路,而依次翻转操作等价于将一个 \(d_{l-1}\to d_r\)(这两个值相等)的环翻转,归纳法可以证明我们的操作能够翻转这张图上任意一个环。

进一步我们能证明:把这张图看成无向图后,任意一条欧拉路对应的字符串 \(S\) 都能得到。

然后我们可以直接贪心,如果下一步走 \(-1\) 可以回来,或当前点没有 \(+1\) 的边就贪心地走。

时间复杂度 \(\mathcal O(|S|)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
int w[MAXN*2];
char str[MAXN];
void solve() {
	scanf("%s",str+1);
	int n=strlen(str+1);
	for(int i=1,x=n;i<=n;++i) {
		if(str[i]=='0') ++w[--x];
		else ++w[x++];
	}
	for(int i=1,x=n;i<=n;++i) {
		if(w[x-1]>1||(w[x-1]&&!w[x])) printf("0"),--w[--x];
		else printf("1"),--w[x++];
	}
	puts("");
}
signed main() {
	int T;
	scanf("%d",&T);
	while(T--) solve();
	return 0;
}



*E. [P9394] 白鹭兰

Problem Link

题目大意

\(n\) 个点 \(m\) 条边的图 \(G\) 分成若干个个集合 \(V_1\sim V_k\),使得任意 \(V_1\cup\dots\cup V_i\) 连通,任意 \(V_{i+1}\cup\cdots\cup V_k\) 连通。

最小化 \(\max |V_i|\)

数据范围:\(n\le 2\times 10^5,m\le 2.3\times 10^5\)

思路分析

如果 \(|V_i|=1\),那么这就是双极定向,\(G\) 点双连通时必然有解。

如果 \(G\) 不点双连通,那么建立圆方树:

  • 如果圆方树是链,那么对每个点双联通分量双极定向,终点设为和下一个点双联通分量的割点即可。
  • 否则我们考虑 \(V_1\to V_k\) 的路径,找到一个不在路径上的方点,这个点内部包含的圆点一定至少在一侧不合法。

因此能够构造当且仅当 \(G\) 是点双联通分量。

不难证明 \(V_i\) 内部连通,否则可以分裂成若干个连通块更优。

那么我们枚举 \(V_1,V_k\),最优解就是把路径上的方点删除,然后每个圆点对应一个连通块,这个连通块就是一个 \(V_i\)

枚举 \(\mathrm{LCA}(V_1,V_k)\),最优路径一定每次走 \(\mathrm{siz}\) 最大的子树,那么简单树形 dp 就能求出方案,然后每个点双连通分量跑一边双极定向即可。

时间复杂度 \(\mathcal O(n+m)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=5e5+5;
vector <int> G[MAXN],E[MAXN];
int n,m,tot;
int dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp;
bool ins[MAXN];
void link(int x,int y) { E[x].push_back(y),E[y].push_back(x); }
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,stk[++tp]=u,ins[u]=true;
	for(int v:G[u]) {
		if(!dfn[v]) {
			tarjan(v),low[u]=min(low[u],low[v]);
			if(low[v]>=dfn[u]) {
				link(u,++tot);
				for(;ins[v];ins[stk[tp--]]=false) link(tot,stk[tp]);
			}
		} else low[u]=min(low[u],dfn[v]);
	}
}
int siz[MAXN],f[MAXN],ans,rt=0,to[MAXN][2];
void dfs1(int u,int fz) {
	siz[u]=(u<=n);
	vector <int> sn;
	for(int v:E[u]) if(v^fz) dfs1(v,u),siz[u]+=siz[v],sn.push_back(v);
	sort(sn.begin(),sn.end(),[&](int x,int y){ return siz[x]>siz[y]; });
	if(sn.empty()) return f[u]=1,void();
	int h=to[u][0]=sn[0],se=to[u][1]=(sn.size()>1?sn[1]:0),vl;
	if(u<=n) {
		f[u]=max(f[h],siz[u]-siz[h]);
		vl=max({f[h],f[se],n-siz[h]-siz[se]});
	} else {
		f[u]=max(f[h],siz[se]);
		vl=max({f[h],f[se],sn.size()>2?siz[sn[2]]:0,n-siz[u]});
	}
	if(ans>=vl) rt=u,ans=vl;
}
int st[MAXN],q;
bool vis[MAXN];
vector <int> grp[MAXN];
void dfs2(int u,int bl) {
	vis[u]=true;
	if(u<=n) grp[bl].push_back(u);
	for(int v:E[u]) if(!vis[v]) dfs2(v,bl);
}
int fa[MAXN],rk[MAXN];
bool inq[MAXN],del[MAXN];
vector <int> ord,pat,Q[MAXN];
void dfs3(int u) {
	low[u]=dfn[u]=++dcnt,rk[dcnt]=u;
	for(int v:G[u]) if(inq[v]) {
		if(!dfn[v]) fa[v]=u,dfs3(v),low[u]=min(low[u],low[v]);
		else low[u]=min(low[u],dfn[v]);
	}
	Q[fa[u]].push_back(u),Q[rk[low[u]]].push_back(u);
}
void dfs4(int u) {
	ord.push_back(u),vis[u]=true;
	for(int v:Q[u]) if(!vis[v]&&!del[v]) dfs4(v);
}
vector<int> solve(vector<int>&V,int S,int T) {
	dcnt=0,ord.clear(),pat.clear();
	for(int u:V) fa[u]=dfn[u]=low[u]=vis[u]=0,inq[u]=1,Q[u].clear();
	dfs3(S);
	for(int u=T;u;u=fa[u]) pat.push_back(u),del[u]=1;
	reverse(pat.begin(),pat.end());
	for(int u:pat) dfs4(u);
	for(int u:V) inq[u]=del[u]=0;
	return ord;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m,tot=n;
	for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
	tarjan(1),ans=n+1,dfs1(1,0);
	for(int x=rt;x;x=to[x][0]) st[++q]=x;
	reverse(st+1,st+q+1);
	if(to[rt][1]) for(int x=to[rt][1];x;x=to[x][0]) st[++q]=x;
	for(int i=2;i<q;i+=2) vis[st[i]]=true;
	for(int i=2;i<q;i+=2) for(int u:E[st[i]]) if(!vis[u]) dfs2(u,u);
	vector <int> wys;
	for(int i=2;i<q;i+=2) {
		auto o=solve(E[st[i]],st[i-1],st[i+1]);
		if(wys.size()) wys.pop_back();
		for(int u:o) wys.push_back(u);
	}
	cout<<ans<<" "<<wys.size()<<"\n";
	for(int i:wys) {
		cout<<grp[i].size()<<" ";
		for(int x:grp[i]) cout<<x<<" "; cout<<"\n";
	}
	return 0;
}



*F. [P10790] 树形图

Problem Link

题目大意

给定 \(n\) 个点 \(m\) 条边的有向图。

  • 一个点 \(u\) 为一类点:当且仅当 \(u\) 到其他的每个点的简单路径都唯一。
  • 一个点 \(u\) 为二类点:删去若干条边后,\(u\) 是一类点,且原有的一类点依然是一类点。

否则一个点是三类点判定每个点是哪类点。

数据范围:\(n\le 10^5,m\le 2\times 10^5\)

思路分析

首先进行缩点,一类点必须是没有入度的 SCC,如果这样的 SCC 不唯一,则所有点为三类点。

否则只需要考虑该 SCC,即考虑图强连通的情况,如果一类点不存在,那么所有点都是二类点。

然后考虑如何判定一个点是一类点:首先建立 dfs 树,如果有横叉边,则一定不为一类点。

否则 dfs 树上只有返祖边,可以归纳证明这样的点一定是一类点。

假设我们找到了一个一类点,考虑求出所有一类点:

考虑每个点被多少条返祖边覆盖,由于图强连通,那么覆盖次数 \(\ge 1\)

如果一个点被 \(>1\) 条返祖边覆盖,那么走到其父亲的路径不唯一。

否则设该返祖边为 \(x\to y\),则 \(u\) 为一类点当且仅当 \(y\) 为一类点,从上到下递推即可。

然后尝试判断哪些点是二类点:

首先我们要知道哪些边可以删除:首先 dfs 树上的边删除后根的连通性改变,肯定不能删除。

其次一条返祖边 \(x\to y\) 如果经过了一个一类点,那么删除后这个点不再是一类点,不能删除。

而其他边都能删除,我们的目标就是要删除若干条可以删除的返祖边,使得剩余的返祖边 \(x\to y\) 唯一,且 \(y\) 是二类点。

先判断经过每个点的不可删除的返祖边是否 \(>1\) 条,然后在 dfs 过程中动态维护维护每个点子树中是否有这样的 \(x\),用树状数组实现即可。

最后我们要找到一个合法的一类点作为根:

考虑其叶子,由于没有横叉边,因此每个叶子的入度为 \(1\),那么对于每个叶子 \(u\),把 \(u\) 向其唯一入点合并(去处自环,不删除重边)。

实际上这就是在 dfs 树上不断缩叶子的过程中,最后停止的时候图上一定只剩唯一节点,这就是一个合法的根,否则说明不存在一类点。

用启发式合并维护该过程。

时间复杂度 \(\mathcal O(m\log n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n,m;
vector <int> G[MAXN];
int dsu[MAXN];
int find(int x) { return x^dsu[x]?dsu[x]=find(dsu[x]):x; }
struct FenwickTree {
	int tr[MAXN],s;
	void init() { memset(tr,0,sizeof(tr)); }
	void add(int x) { for(;x<=n;x+=x&-x) ++tr[x]; }
	int qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
}	TR;
int dfn[MAXN],low[MAXN],dcnt,stk[MAXN],tp,col[MAXN],scnt;
bool ins[MAXN],ind[MAXN],inq[MAXN],vis[MAXN];
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,ins[stk[++tp]=u]=true;
	for(int v:G[u]) {
		if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
		else if(ins[v]) low[u]=min(low[u],low[v]);
	}
	if(dfn[u]==low[u]) {
		++scnt;
		while(ins[u]) col[stk[tp]]=scnt,ins[stk[tp--]]=false;
	}
}
int deg[MAXN];
vector <int> in[MAXN];
unordered_map <int,int> S[MAXN];
bool chk(int u) {
	memset(ins,0,sizeof(ins));
	memset(vis,0,sizeof(vis));
	bool ok=1;
	function<void(int)> dfs=[&](int x) {
		vis[x]=ins[x]=true;
		for(int y:G[x]) {
			if(!vis[y]) dfs(y);
			else ok&=ins[y];
		}
		ins[x]=false;
	};
	dfs(u);
	for(int i=1;i<=n;++i) ok&=vis[i];
	return ok;
}
int getrt() {
	for(int i=1;i<=n;++i) dsu[i]=i;
	for(int i=1;i<=n;++i) for(int j:G[i]) {
		++S[i][j],in[j].push_back(i),++deg[j];
	}
	for(int i=1;i<=n;++i) if(!deg[i]) return chk(i)?i:0;
	queue <int> Q;
	for(int i=1;i<=n;++i) if(deg[i]==1) Q.push(i);
	while(Q.size()) {
		int x=0,y=Q.front(); Q.pop();
		if(deg[y]!=1||dsu[y]!=y) continue;
		for(int k:in[y]) if(find(k)!=y) {
			x=find(k); break;
		}
		S[x].erase(y);
		auto it=S[y].find(x);
		if(it!=S[y].end()) {
			deg[x]-=it->second,S[y].erase(it);
			if(deg[x]==1) Q.push(x);
		}
		if(S[x].size()<S[y].size()) swap(S[x],S[y]);
		for(auto e:S[y]) if(e.second) S[x][e.first]+=e.second;
		dsu[y]=x;
	}
	for(int i=1;i<=n;++i) if(dsu[i]==i) return chk(i)?i:0;
	return 0;
}
int fa[MAXN],L[MAXN],R[MAXN],k,x[MAXN<<1],y[MAXN<<1];
int dep[MAXN],cov[MAXN],del[MAXN];
vector <int> E[MAXN],T[MAXN];
void dfs0(int u) {
	vis[u]=true,L[u]=++dcnt;
	for(int v:G[u]) if(inq[v]) {
		if(!vis[v]) fa[v]=u,dep[v]=dep[u]+1,T[u].push_back(v),dfs0(v);
		else ++k,x[k]=u,y[k]=v,E[v].push_back(u);
	}
	R[u]=dcnt;
}
bool f[MAXN],g[MAXN],rsv[MAXN];
void dfs1(int u) {
	if(cov[u]>0&&f[y[cov[u]]]) f[u]=g[u]=true,rsv[cov[u]]=true;
	for(int v:T[u]) dfs1(v);
}
void dfs2(int u) {
	if(~del[u]) {
		if(del[u]) g[u]|=g[y[del[u]]];
		else g[u]|=(TR.qry(R[u])>TR.qry(L[u]-1));
	}
	if(g[u]) for(int v:E[u]) TR.add(L[v]);
	for(int v:T[u]) dfs2(v);
}
void solve() {
	cin>>n>>m;
	for(int i=1;i<=n;++i) {
		G[i].clear(),E[i].clear(),T[i].clear();
		S[i].clear(),in[i].clear();
		dfn[i]=low[i]=cov[i]=del[i]=deg[i]=0;
		ins[i]=ind[i]=f[i]=g[i]=0;
	}
	dcnt=scnt=tp=0;
	for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back(v);
	for(int i=1;i<=n;++i) if(!dfn[i]) tarjan(i);
	int id=0;
	for(int i=1;i<=n;++i) for(int j:G[i]) if(col[i]^col[j]) ind[col[j]]=true;
	for(int i=1;i<=scnt;++i) if(!ind[i]) id=(!id?i:-1);
	if(id<=0) {
		for(int i=1;i<=n;++i) cout<<"3"; cout<<"\n";
		return ;
	}
	for(int i=1;i<=n;++i) inq[i]=(col[i]==id);
	int rt=getrt();
	if(!rt) {
		for(int i=1;i<=n;++i) cout<<"32"[inq[i]]; cout<<"\n";
		return ;
	}
	memset(vis,0,sizeof(vis));
	memset(rsv,0,sizeof(rsv));
	dcnt=k=0,dep[rt]=0,dfs0(rt);
	for(int i=1;i<=n;++i) dsu[i]=i;
	for(int e=1;e<=k;++e) {
		for(int u=find(x[e]);dep[u]>dep[y[e]];u=find(fa[u])) {
			if(!cov[u]) cov[u]=e;
			else cov[u]=-1,dsu[u]=find(fa[u]);
		}
	}
	f[rt]=g[rt]=true;
	for(int u:T[rt]) dfs1(u);
	for(int i=1;i<=n;++i) dsu[i]=i;
	for(int e=1;e<=k;++e) if(rsv[e]) {
		for(int u=find(x[e]);dep[u]>dep[y[e]];u=find(fa[u])) {
			if(!del[u]) del[u]=e;
			else del[u]=-1,dsu[u]=find(fa[u]);
		}
	}
	TR.init();
	for(int u:E[rt]) TR.add(L[u]);
	for(int u:T[rt]) dfs2(u);
	for(int i=1;i<=n;++i) {
		if(inq[i]) cout<<"321"[f[i]+g[i]];
		else cout<<"3";
	}
	cout<<"\n";
}
signed main() {
	ios::sync_with_stdio(false);
	int id,cas;
	cin>>id>>cas;
	while(cas--) solve();
	return 0;
}



Round #72 - 20250417

A. [P3308] LIS

Problem Link

题目大意

给定 \(n\) 个元素 \((a_i,b_i,c_i)\),删除若干元素使得 \(a\) 的 LIS 减小,且删除元素的 \(\sum b\) 最小,如果有多少种方案,求删除元素的 \(\{c\}\) 字典序最小的一组。

数据范围:\(n\le 700\)

思路分析

首先求 \(\min \sum b\),把元素按 dp 结果分层,那么一组合法方案相当于 \(f_i=1\)\(f_i=|\mathrm{LIS}|\) 的点不连通。

那么只要求最小割就可以了。

然后加上 \(\{c\}\) 的限制,即求字典序最小的最小割。

首先考虑哪些边可以被加入最小割。

首先存在一种方案使得最小割里的边都被满流,进一步,一组最大流使得某条边未满流,则该边不可能属于最小割:

将这条边边权减小极小值 \(\epsilon\),最大流不变,但如果存在一组过该边的最小割,则最小割权值 \(-\epsilon\),矛盾。

因此当前的非满流边必然不在最小割中。

那么对于一条满流边 \(u\to v\),如果可以在残量网络上找到过 \((u,v)\) 的环,则 \(u\to v\) 不再满流,这些边也不能选。

那么对残量网络强连通缩点,SCC 内部的边必然不在最小割中。

且可以证明缩点后取出两个连通集合 \((S,T)\),他们之间的边一定构成一组最小割。

那么这题中我们也进行缩点,然后按 \(c\) 从小到大考虑每条边 \((u,v)\),如果将其加入最小割,则 \(u\) 的前驱都 \(\in S\)\(v\) 的后继都 \(\in T\),按这种方式标记即可。

时间复杂度 \(\mathcal O(\mathrm{Flow}(n,n^2))\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXV=1405,MAXE=2e5+5;
const ll inf=1e15;
struct Edge {
	int v,lst; ll f;
}	G[MAXE];
int S,T,tot=1,hd[MAXV],cur[MAXV],dep[MAXV];
void init() { tot=1,memset(hd,0,sizeof(hd)); }
void adde(int u,int v,ll w) { G[++tot]={v,hd[u],w},hd[u]=tot; }
void link(int u,int v,ll w) { adde(u,v,w),adde(v,u,0); }
bool BFS() {
	memcpy(cur,hd,sizeof(cur)),memset(dep,-1,sizeof(dep));
	queue <int> Q;
	Q.push(S),dep[S]=0;
	while(!Q.empty()) {
		int u=Q.front(); Q.pop();
		for(int i=hd[u];i;i=G[i].lst) if(G[i].f&&dep[G[i].v]==-1) {
			dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
		}
	}
	return ~dep[T];
}
ll dfs(int u,ll f) {
	if(u==T) return f;
	ll r=f;
	for(int i=cur[u];i;i=G[i].lst) {
		int v=G[cur[u]=i].v;
		if(G[i].f&&dep[v]==dep[u]+1) {
			ll g=dfs(v,min(r,G[i].f));
			if(!g) dep[v]=-1;
			G[i].f-=g,G[i^1].f+=g,r-=g;
		}
		if(!r) return f;
	}
	return f-r;
}
ll Dinic() {
	ll f=0;
	while(BFS()) f+=dfs(S,inf);
	return f;
}
const int MAXN=705;
int n,a[MAXN],b[MAXN],c[MAXN],dp[MAXN],ed[MAXN];
int dfn[MAXV],low[MAXV],dcnt,stk[MAXV],tp,bl[MAXV],scnt,cl[MAXV];
bool ins[MAXV];
void tarjan(int u) {
	dfn[u]=low[u]=++dcnt,stk[++tp]=u,ins[u]=true;
	for(int i=hd[u];i;i=G[i].lst) if(G[i].f) {
		int v=G[i].v;
		if(!dfn[v]) tarjan(v),low[u]=min(low[u],low[v]);
		else if(ins[v]) low[u]=min(low[u],dfn[v]);
	}
	if(low[u]==dfn[u]) for(++scnt;ins[u];ins[stk[tp--]]=false) bl[stk[tp]]=scnt;
}
void dfs(int u,int o) {
	if(~cl[u]) return ; cl[u]=o;
	for(int i=hd[u];i;i=G[i].lst) if(G[i^o].f||bl[G[i].v]==bl[u]) dfs(G[i].v,o);
}
void solve() {
	scanf("%d",&n),init(),S=2*n+1,T=2*n+2;
	for(int i=1;i<=n;++i) scanf("%d",&a[i]);
	for(int i=1;i<=n;++i) scanf("%d",&b[i]);
	for(int i=1;i<=n;++i) scanf("%d",&c[i]);
	int mx=0;
	for(int i=1;i<=n;++i) {
		dp[i]=0;
		for(int j=1;j<i;++j) if(a[j]<a[i]) dp[i]=max(dp[i],dp[j]);
		mx=max(mx,++dp[i]);
	}
	for(int i=1;i<=n;++i) {
		link(i,i+n,b[i]),ed[i]=tot-1;
		if(dp[i]==1) link(S,i,inf);
		if(dp[i]==mx) link(i+n,T,inf);
		for(int j=1;j<i;++j) if(a[j]<a[i]&&dp[j]==dp[i]-1) link(j+n,i,inf);
	}
	printf("%lld ",Dinic());
	for(int i=1;i<=2*n+2;++i) if(!dfn[i]) tarjan(i);
	vector <int> ord,ans;
	for(int i=1;i<=n;++i) ord.push_back(i);
	sort(ord.begin(),ord.end(),[&](int x,int y){ return c[x]<c[y]; });
	memset(cl,-1,sizeof(cl)),dfs(S,0),dfs(T,1);
	for(int i:ord) if(!G[ed[i]].f&&bl[i]!=bl[i+n]&&cl[i]!=1&&cl[i+n]!=0) {
		ans.push_back(i),dfs(i,0),dfs(i+n,1);
	}
	sort(ans.begin(),ans.end());
	printf("%d\n",(int)ans.size());
	for(int u:ans) printf("%d ",u); puts("");
	memset(dfn,0,sizeof(dfn)),dcnt=scnt=0;
}
signed main() {
	int _; scanf("%d",&_);
	while(_--) solve();
	return 0;
}



B. [AGC036D] Negative Cycle

Problem Link

题目大意

给定 \(n\) 个点的有向图,包含 \(i\to i+1\) 权值为 \(0\) 的边,以及对于每对 \((i,j)\)\(i\to j\),权值为 \(\mathrm{sgn}(i-j)\) 的边。

现在删除若干第二类边(删除每条边有对应代价)使得图无负环,最小化总花费。

数据范围:\(n\le 500\)

思路分析

无负环难以刻画,不妨看成对应的差分约束模型有解。

那么改写每条边对应的限制:

  • \(x_i\ge x_{i+1}\)
  • 如果 \(i>j\),则 \(x_i+1\ge x_j\)
  • 如果 \(i<j\),则 \(x_i-1\ge x_j\)

我们要构造一组 \(\{x\}\) 使得满足后两个条件的边权值和最大。

\(x\) 按相等权值分为若干段,则同一段内部的二类边要删除,相差 \(>1\) 段的三类便要删除。

\(f_{i,j}\) 表示最后一段为 \([i,j]\) 的最优解,转移枚举下一段 \([j+1,k]\),二维前缀和快速计算权值即可。

时间复杂度 \(\mathcal O(n^3)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=505;
int n;
ll tot=0,a[MAXN][MAXN],S[MAXN][MAXN],w[MAXN][MAXN],f[MAXN][MAXN];
ll val(int i,int j,int k) { //(i,j] -> (j,k]
	ll sum=w[j+1][k];
	sum+=S[j][k]-S[j][j];
	sum+=S[k][j]-S[j][j]-S[k][i]+S[j][i];
	return sum;
}
signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) {
		if(i!=j) scanf("%lld",&a[i][j]),tot+=a[i][j];
		S[i][j]=a[i][j]+S[i-1][j]+S[i][j-1]-S[i-1][j-1];
	}
	for(int l=1;l<=n;++l) for(int r=l;r<=n;++r) {
		w[l][r]=w[l][r-1];
		for(int i=l;i<=r;++i) w[l][r]+=a[r][i];
	}
	for(int i=1;i<=n;++i) f[0][i]=w[1][i];
	for(int j=1;j<=n;++j) for(int i=0;i<j;++i) for(int k=j+1;k<=n;++k) {
		f[j][k]=max(f[j][k],f[i][j]+val(i,j,k));
	}
	ll ans=0;
	for(int i=0;i<n;++i) ans=max(ans,f[i][n]);
	printf("%lld\n",tot-ans);
	return 0;
}



C. [QOJ2070] Heavy Stones

Problem Link

题目大意

给定 \(n\) 堆石子 \(a_1\sim a_n\),你可以从某堆石子出发,每次合并左侧或右侧相邻的一堆石子,代价为合并后的石子总数,对每个起点输出合并所有石子的最小总代价。

数据范围:\(n\le 10^6\)

思路分析

首先对于单个起点,这就是一个树上 Exchange Argument,但不可能对每个起点都做一遍。

注意到树是两条链,基本可以分成独立的两部分,只有在向根节点合并时互相有影响。

我们只要把 Exchange Argument 中每次向根合并的元素找到,然后把左右两条链上这样的元素归并排序,就能得到答案了。

处理一条链上向根合并的元素是经典的,从下到上单调栈即可,如果栈顶比当前元素优,就把他们合并。

从前往后从后往前处理出这些元素,均摊修改 \(\mathcal O(n)\) 次,可以平衡树维护,或者离线下来线段树。

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5,N=1<<19;
struct info {
	ll ct,su,vl;
	inline info operator +(const info &o) { return {ct+o.ct,su+o.su,vl+su*o.ct+o.vl}; }
	inline bool operator <(const info &o) { return su*o.ct<o.su*ct; }
}	f[MAXN*2],tr[N<<1];
void upd(int x,info o) {
	for(tr[x+=N]=o,x>>=1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
}
vector <int> op[MAXN];
int n,a[MAXN],st[MAXN],id[MAXN*2],rk[MAXN*2];
signed main() {
	scanf("%d",&n);
	for(int i=1;i<=n;++i) scanf("%d",&a[i]),f[i]=f[i+n]={1,a[i],a[i]};
	int tp=0;
	for(int i=1;i<n;++i) {
		while(tp&&f[st[tp]]<f[i]) op[i+1].push_back(-st[tp]),f[i]=f[i]+f[st[tp--]];
		op[i+1].push_back(i),st[++tp]=i;
	}
	tp=0;
	for(int i=n;i>1;--i) {
		while(tp&&f[st[tp]]<f[i+n]) op[i].push_back(st[tp]),f[i+n]=f[i+n]+f[st[tp--]];
		op[i].push_back(-i-n),st[++tp]=i+n;
	}
	for(int i=1;i<=tp;++i) op[1].push_back(st[i]);
	iota(id+1,id+2*n+1,1);
	sort(id+1,id+2*n+1,[&](int x,int y){ return f[x]<f[y]; });
	for(int i=1;i<=2*n;++i) rk[id[i]]=i;
	for(int i=1;i<=n;++i) {
		for(int x:op[i]) x<0?upd(rk[-x],{0,0,0}):upd(rk[x],f[x]);
		printf("%lld ",tr[1].vl+1ll*(n-1)*a[i]);
	}
	puts("");
	return 0;
}



D. [CF925F] Parametric Circulation

题目大意

给定 \(n\) 个点 \(m\) 条边的有向图,每条边流量的上下界都是关于 \(z\) 的一次函数,求 \(z\in[0,1]\) 随机时该图有上下界循环流的概率。

数据范围:\(n\le 1000,m\le 2000\)

思路分析

首先建出上下界循环流对应的图,要求最大流等于正度数之和。

我们知道最大流等于最小割,而图的结构不变,因此图上可能的割集不变。

而这些割集的权值都是关于 \(z\) 的一次函数,因此图的最小割(最大流)是这些一次函数最小值,即一个上凸函数。

而正度数之和是关于 \(z\) 的一次函数,且始终大于等于最大流大小,因此答案就是该一次函数和凸壳相切的长度。

可以先三分斜率然后二分范围。

时间复杂度 \(\mathcal O(\mathrm{Flow}(n,m)\log\epsilon)\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
namespace F {
const int MAXV=1005,MAXE=10005;
const ll inf=1e18;
struct Edge {
	int v,lst; ll f;
}	G[MAXE];
int S,T,tot=1,hd[MAXV],cur[MAXV],dep[MAXV];
void init() { tot=1,memset(hd,0,sizeof(hd)); }
void adde(int u,int v,ll w) { G[++tot]={v,hd[u],w},hd[u]=tot; }
void link(int u,int v,ll w) { adde(u,v,w),adde(v,u,0); }
bool BFS() {
	memcpy(cur,hd,sizeof(cur)),memset(dep,-1,sizeof(dep));
	queue <int> Q;
	Q.push(S),dep[S]=0;
	while(!Q.empty()) {
		int u=Q.front(); Q.pop();
		for(int i=hd[u];i;i=G[i].lst) if(G[i].f&&dep[G[i].v]==-1) {
			dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
		}
	}
	return ~dep[T];
}
ll dfs(int u,ll f) {
	if(u==T) return f;
	ll r=f;
	for(int i=cur[u];i;i=G[i].lst) {
		int v=G[cur[u]=i].v;
		if(G[i].f&&dep[v]==dep[u]+1) {
			ll g=dfs(v,min(r,G[i].f));
			if(!g) dep[v]=-1;
			G[i].f-=g,G[i^1].f+=g,r-=g;
		}
		if(!r) return f;
	}
	return f-r;
}
ll Dinic() {
	ll f=0;
	while(BFS()) f+=dfs(S,inf);
	return f;
}
}
const int MAXN=2005,V=1e8;
int n,m,u[MAXN],v[MAXN];
ll a[MAXN],b[MAXN],c[MAXN],d[MAXN];
ll deg[MAXN];
ll chk(ll x) {
	int s=F::S=n+1,t=F::T=n+2;
	F::init(),memset(deg,0,sizeof(deg));
	for(int i=1;i<=m;++i) {
		ll l=a[i]*x+b[i],r=c[i]*x+d[i];
		F::link(u[i],v[i],r-l),deg[u[i]]-=l,deg[v[i]]+=l;
	}
	ll rs=0;
	for(int i=1;i<=n;++i) {
		if(deg[i]>0) F::link(s,i,deg[i]),rs+=deg[i];
		else F::link(i,t,-deg[i]);
	}
	return rs-F::Dinic();
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;++i) cin>>u[i]>>v[i]>>a[i]>>b[i]>>c[i]>>d[i],b[i]*=V,d[i]*=V;
	int l=0,r=V,p=-1,bg,ed;
	while(r-l>=3) {
		int mid=(l+r)>>1;
		if(chk(mid)>chk(mid+1)) l=mid+1;
		else r=mid;
	}
	for(int i=l;i<=r;i++) if(!chk(i)) p=i;
	if(p==-1) return cout<<"0\n",0;
	l=0,r=p-1,bg=p;
	while(l<=r) {
		int mid=(l+r)>>1;
		if(!chk(mid)) bg=mid,r=mid-1;
		else l=mid+1;
	}
	l=p+1,r=V,ed=p;
	while(l<=r) {
		int mid=(l+r)>>1;
		if(!chk(mid)) ed=mid,l=mid+1;
		else r=mid-1;
	}
	cout<<fixed<<setprecision(20)<<1.*(ed-bg)/V<<"\n";
	return 0;
}



*E. [P7417] Minimizing Edges

Problem Link

题目大意

给定 \(n\) 个点 \(m\) 条边的无向图,构造一张边数最少的图使得 \(\forall (u,x)\),两图中同时存在或不存在 \(1\to u\) 长度为 \(x\) 的路径。

数据范围:\(n\le 10^5,m\le 2\times 10^5\)

思路分析

由于我们可以在同一条边上来回移动,那么只要到每个点奇数长度和偶数长度最短路相同即可。

设这两条最短路为 \((x,y)\),其中 \(x<y\),那么这样的一个点只能和 \((x-1,y-1)\) 连接,或者同时连接 \((x+1,y-1),(x-1,y+1)\)

特别的,如果 \(y=x+1\),那么连接另一个 \((x,y)\) 以及一个 \((x-1,y+1)\)

我们可以把所有点按 \(x+y\) 分组,同一组内的问题相对独立,按 \(x\) 从小到大扫描:

首先如果存在 \((x-1,y-1)\) 那么优先连接肯定更优,否则连接 \((x-1,y+1)\)\((x+1,y-1)\)

如果 \((x+1,y-1)\) 已经和当前点连接,那么继续连接 \((x-1,y+1)\) 而非 \((x-1,y-1)\),因为此时不断连接到 \((x,x+1)\),只需要一条边就能解决两个点。

模拟上述过程即可,注意特判二分图和 \(1\) 有自环的情况。

时间复杂度 \(\mathcal O(n\log n)\)

代码呈现

#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,inf=1e9;
vector <int> G[MAXN];
map <int,int> f[MAXN],g[MAXN];
int n,m,d[MAXN][2];
void solve() {
	scanf("%d%d",&n,&m);
	for(int i=0;i<=n;++i) f[i].clear(),g[i].clear(),G[i].clear(),d[i][0]=d[i][1]=inf;
	for(int i=1,u,v;i<=m;++i) scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
	queue <array<int,2>> Q; d[1][0]=0,Q.push({1,0});
	while(Q.size()) {
		int u=Q.front()[0],r=Q.front()[1]^1; Q.pop();
		for(int v:G[u]) if(d[v][r]==inf) d[v][r]=d[u][r^1]+1,Q.push({v,r});
	}
	if(d[1][1]==inf) return printf("%d\n",n-1),void();
	if(d[1][1]==1) return printf("%d\n",n),void();
	vector <array<int,2>> P;
	for(int i=1;i<=n;++i) {
		int x=min(d[i][0],d[i][1]),y=max(d[i][0],d[i][1]);
		if(!f[x][y]++&&i>1) P.push_back({x+y,x});
	}
	int ans=0;
	sort(P.begin(),P.end());
	for(auto it:P) {
		int x=it[1],y=it[0]-it[1],sz=f[x][y],pr=g[x-1][y+1];
		ans+=max(0,sz-pr); //to right or up
		if(f[x-1][y-1]) sz=min(sz,pr);
		ans+=(y==x+1?(sz+1)/2:g[x][y]=sz); //to left
	}
	printf("%d\n",ans);
}
signed main() {
	int _; scanf("%d",&_);
	while(_--) solve();
	return 0;
}



*F. [P8501] 二次整数规划问题

Problem Link

题目大意

构造 \(x_1\sim x_n\),满足 \(x_i\in [l_i,r_i]\subseteq[1,k]\) 以及 \(m\) 条限制形如 \(|x_u-x_v|\le w\)

\(q\) 次询问给定 \(v_1\sim v_k\),其中 \(v_1=v_k=0\),最大化 \(10^6\sum_{i,j}[|x_i-x_j|\le 1]+\sum v_i\sum_j [x_j=i]\)

数据范围:\(n\le 600,m\le 3n,q\le 3\times 10^5,k\le 5\)

思路分析

只分析 \(k=5\) 的情况。

首先显然填 \(1,5\) 的元素越少越好,可以预处理出这样的元素。

剩余的元素在 \(2,3,4\) 中选择,则答案为 \(\sum v_ic_i+10^6(n^2-2(c_2c_4-c_1c_4-c_2c_5-c_1c_5-c_3c_1-c_3c_5))\)

那么答案可以看成一个有关 \(x=c_2,y=c_4\) 的函数 \(f(x,y)=axy+bx+cy+d\),其中 \(a=-2\times 10^6\)

转写成求 \(\max a(x-x_0)(y-y_0)\),即最小化 \((x-x_0)(y-y_0)\)

把所有的 \((x,y)\) 画在平面上,设他们占据的范围为 \([x_L,x_R]\times [y_L,y_R]\),由于这些限制可以把一些填 \(2,4\) 的点直接调成 \(3\),因此一定能取到 \((x_L,y_L),(x_R,y_L)(x_L,y_R)\)

如果这个矩形平移 \((-x_0,-y_0)\) 后经过二四象限,则答案一定在 \((x_L,y_R)\)\((x_R,y_L)\) 上取到。

否则要么全在第一象限要么全在第三象限,第一种情况答案在 \((x_L,y_L)\) 上取到。

否则相当于求 \((x,y)\) 的上凸壳,类似 最小乘积生成树,分治构造凸包,每次求出距离当前区间左右端点最远的点,这个点一定在凸包上。

而这个问题相当于求一组解最大化 \(z_2c_2+z_4c_4\),先转成最小化 \(z_4c_2+(z_2+z_4)c_3+z_2c_4\)

然后把 \(w=0\) 的限制对应连通块缩起来,限制是有些点不能一个填 \(2\) 一个填 \(4\),这是经典的切糕模型,网络流解决。

可以证明凸壳上点数不会超过 \(\mathcal O(n^{2/3})\) 级别。

时间复杂度 \(\mathcal O(n^{2/3}(q+\mathrm{Flow}(n,m)))\)

代码呈现

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=605,Z=1e6,inf=1e9;
int k,n,m,q,L[MAXN],R[MAXN],c[6];
int dsu[MAXN],bl[MAXN],id[MAXN],sz[MAXN],tot;
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
vector <array<int,2>> vc,lim;
struct Flow {
static const int MAXV=1205,MAXE=2e5+5;
struct Edge {
	int v,f,lst;
}	G[MAXE];
int S,T,ec=1,vc,hd[MAXV],cur[MAXV],dep[MAXV];
void init() { ec=1,memset(hd,0,(vc+1)<<2); }
void adde(int u,int v,int w) { G[++ec]={v,w,hd[u]},hd[u]=ec; }
void link(int u,int v,int w) { adde(u,v,w),adde(v,u,0); }
bool BFS() {
	memcpy(cur,hd,(vc+1)<<2),memset(dep,-1,(vc+1)<<2);
	queue <int> Q;
	Q.push(S),dep[S]=0;
	while(!Q.empty()) {
		int u=Q.front(); Q.pop();
		for(int i=hd[u];i;i=G[i].lst) if(G[i].f&&dep[G[i].v]==-1) {
			dep[G[i].v]=dep[u]+1,Q.push(G[i].v);
		}
	}
	return ~dep[T];
}
int dfs(int u,int f) {
	if(u==T) return f;
	int r=f;
	for(int i=cur[u];i;i=G[i].lst) {
		int v=G[cur[u]=i].v;
		if(G[i].f&&dep[v]==dep[u]+1) {
			int g=dfs(v,min(r,G[i].f));
			if(!g) dep[v]=-1;
			G[i].f-=g,G[i^1].f+=g,r-=g;
		}
		if(!r) return f;
	}
	return f-r;
}
int Dinic() {
	int f=0;
	while(BFS()) f+=dfs(S,inf);
	return f;
}
}	F;
void build(array<int,2>a,array<int,2>b) {
	int wx=a[1]-b[1],wy=b[0]-a[0];
	int s=F.S=2*tot+1,t=F.T=F.vc=2*tot+2;
	F.init();
	for(int i=1;i<=tot;++i) {
		F.link(s,i,L[id[i]]==2?sz[i]*wy:inf);
		F.link(i,i+tot,sz[i]*(wy+wx));
		F.link(i+tot,t,R[id[i]]==4?sz[i]*wx:inf);
	}
	for(auto e:lim) F.link(e[0]+tot,e[1],inf),F.link(e[1]+tot,e[0],inf);
	F.Dinic();
	array<int,2>o{c[2],c[4]};
	for(int i=1;i<=tot;++i) {
		if(F.dep[i]==-1) o[0]+=sz[i];
		if(~F.dep[i+tot]) o[1]+=sz[i];
	}
	if(o[0]*wx+o[1]*wy>a[0]*wx+a[1]*wy) vc.push_back(o),build(a,o),build(o,b);
}
void solve() {
	cin>>k>>n>>m>>q;
	for(int i=1;i<=n;++i) cin>>L[i]>>R[i];
	vector <array<int,3>> edg;
	for(int i=1,u,v,w;i<=m;++i) {
		cin>>u>>v>>w,edg.push_back({u,v,w});
	}
	for(int t=1;t<=n;++t) for(auto e:edg) {
		int u=e[0],v=e[1],w=e[2];
		L[v]=max(L[v],L[u]-w),R[v]=min(R[v],R[u]+w);
		L[u]=max(L[u],L[v]-w),R[u]=min(R[u],R[v]+w);
	}
	for(int i=1;i<=n;++i) {
		if(R[i]>1&&L[i]<k) L[i]=max(L[i],2),R[i]=min(R[i],k-1);
		if(L[i]==R[i]) ++c[L[i]];
	}
	if(k==3) vc={{n-c[1]-c[3],n-c[1]-c[3]}};
	else if(k==4) vc={{c[2],n-c[1]-c[2]-c[4]},{n-c[1]-c[3]-c[4],c[3]}};
	else {
		int m2=0,m4=0;
		for(int i=1;i<=n;++i) m2+=L[i]==2,m4+=R[i]==4;
		vc={{c[2],c[4]},{c[2],m4},{m2,c[4]}};
		tot=0,lim.clear(),iota(dsu+1,dsu+n+1,1);
		for(auto e:edg) if(!e[2]) dsu[find(e[0])]=find(e[1]);
		for(int i=1;i<=n;++i) if(L[i]!=R[i]&&dsu[i]==i) id[++tot]=i,bl[i]=tot;
		for(int i=1;i<=n;++i) if(L[i]!=R[i]) ++sz[bl[find(i)]];
		for(auto e:edg) if(e[2]==1) {
			int x=bl[find(e[0])],y=bl[find(e[1])];
			if(x&&y) lim.push_back({x,y});
		}
		build(vc[1],vc[2]);
	}
	while(q--) {
		array <ll,6> vt={0,0,0,0,0,0};
		for(int i=2;i<k;++i) cin>>vt[i];
		ll ans=0;
		for(auto o:vc) {
			auto e=c;
			e[2]=o[0],e[k-1]=o[1];
			if(k==5) e[3]=n-e[1]-e[2]-e[4]-e[5];
			ll s=0;
			for(int i=1;i<=k;++i) s+=vt[i]*e[i]+1ll*(e[i]+2*e[i-1])*e[i]*Z;
			ans=max(ans,s);
		}
		cout<<ans<<"\n";
	}
	memset(sz,0,sizeof(sz)),memset(c,0,sizeof(c)),memset(bl,0,sizeof(bl)),tot=0;
}
signed main() {
	ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
	int ty,_; cin>>ty>>_;
	while(_--) solve();
	return 0;
}
posted @ 2025-05-12 20:53  DaiRuiChen007  阅读(135)  评论(0)    收藏  举报