2025 NOI 做题记录(三)
个人训练赛题解(十五)
\(\text{By DaiRuichen007}\)
Round #69 - 20250409
A. [QOJ5091] 大冬天题
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
交互器有一个未知变量 \(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] 雷同
题目大意
给定 \(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] 挑战积和式
题目大意
给定 \(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
题目大意
给定长度为 \(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
题目大意
定义一个数组 \(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
题目大意
你需要构造一棵 \(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] 棋盘
题目大意
在无穷大二维平面上选出 \(\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
题目大意
给定 \(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] 没有创意的题目名称
题目大意
给定 \(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] 密码锁
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 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] 白鹭兰
题目大意
把 \(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] 树形图
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 \(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
题目大意
给定 \(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] 二次整数规划问题
题目大意
构造 \(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;
}

浙公网安备 33010602011771号