2026 NOI 做题记录(十九)
\(\text{By DaiRuiChen007}\)
A. [QOJ4808] Great Party (2.5)
手玩发现 \(n=2\) 时必须 \(a_1=a_2\) 后手必胜,从而 \(n=3\) 时先手必胜,因为可以操作最大的一堆从而向 \(n=2\) 递归。
那么对于更大的 \(n\),如果 \(n\) 是奇数则先手必胜,如果 \(n\) 是偶数那么谁先删掉一堆石子就输,即 \(\oplus (a_i-1)=0\) 时后手输。
直接莫队统计。
时间复杂度 \(\mathcal O(n\sqrt q)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
struct qry { int l,r,i; } b[MAXN];
ll s,z[MAXN];
int n,q,a[MAXN],c[1<<21|5],B;
void add(int x) { s+=c[x]++; }
void del(int x) { s-=--c[x]; }
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>q,B=n/sqrt(q)+1;
for(int i=1;i<=n;++i) cin>>a[i],a[i]=a[i-1]^(2*a[i]-1);
for(int i=1;i<=q;++i) cin>>b[i].l>>b[i].r,--b[i].l,b[i].i=i;
sort(b+1,b+q+1,[&](auto x,auto y){ return x.l/B!=y.l/B?x.l<y.l:(x.l/B%2?x.r>y.r:x.r<y.r); });
for(int i=1,l=1,r=0;i<=q;++i) {
while(r<b[i].r) add(a[++r]);
while(l>b[i].l) add(a[--l]);
while(l<b[i].l) del(a[l++]);
while(r>b[i].r) del(a[r--]);
z[b[i].i]=1ll*(r-l+1)*(r-l)/2-s;
}
for(int i=1;i<=q;++i) cout<<z[i]<<"\n";
return 0;
}
B. [QOJ4786] Balance (3)
平衡矩阵满足 \(a_{i,j}=x_i+y_j\),那么对 \(x_i+y_j\ge a_{i,j},\min \sum x_i+y_i\) 的线性规划对偶,得到 \(\sum_i z_{i,j}\le 1,\sum_j z_{i,j}\le 1,\max \sum z_{i,j}a_{i,j}\)。
即每行每列选一个元素,使得 \(\sum a\) 最大,显然这就是二分图最大权匹配。
求出最大匹配后可以差分约束,如果了解 KM 算法,那么 \(x,y\) 实际上就是一组可行顶标。
时间复杂度 \(\mathcal O(n^3)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[55][55],f[55],g[55],w[55],p[55];
bool ix[55],iy[55];
bool dfs(int x) {
if(ix[x]) return 0; ix[x]=1;
for(int y=1;y<=n;++y) if(!iy[y]) {
int t=f[x]+g[y]-a[x][y];
if(!t) {
iy[y]=1; if(!p[y]||dfs(p[y])) return p[y]=x,1;
} else w[y]=min(w[y],t);
}
return 0;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) cin>>a[i][j],f[i]=max(f[i],a[i][j]);
for(int u=1;u<=n;++u) {
for(int i=1;i<=n;++i) ix[i]=iy[i]=0,w[i]=2e9;
if(dfs(u)) continue;
while(1) {
int y=0;
for(int i=1;i<=n;++i) if(!iy[i]&&(!y||w[y]>w[i])) y=i;
for(int i=1,v=w[y];i<=n;++i) {
if(ix[i]) f[i]-=v;
if(iy[i]) g[i]+=v;
else w[i]-=v;
}
int x=p[y];
if(!x) break;
ix[x]=iy[y]=1;
for(int i=1;i<=n;++i) if(!iy[i]) w[i]=min(w[i],f[x]+g[i]-a[x][i]);
}
for(int i=1;i<=n;++i) ix[i]=iy[i]=0;
dfs(u);
}
int z=0;
for(int i=1;i<=n;++i) z+=f[i]+g[i];
cout<<z*n<<"\n";
for(int i=1;i<=n;++i) for(int j=1;j<=n;++j) cout<<f[i]+g[j]<<" \n"[j==n];
return 0;
}
C. [QOJ8559] k-coloring (4)
特判 \(k=1\),对于 \(k=2\),考虑增量构造,对于原路径上的一个点 \(u\),替换为 \([u,v,u]\) 即可加入边 \((u,v)\),那么这样就完成了构造。
对于 \(k=3\),先考虑对树构造,dfs 时按深度分层,深度为奇数的点 \(u\) 在向下 dfs 的过程中就标记 \((u,fa_u)\),否则在回溯时再标记 \((u,fa_u)\)。
实现一个 dfs 函数 $f(u) $ 记录进出 \(u\) 时路径长度 \(\bmod 3\) 的值,对于深度为奇数的点要求 \(f(u)\) 是 \(\{0,1,2\}\to\{0,1,2\}\),否则要求是 \(\{0,1\}\to \{0,1\}\),简单分讨容易实现,非树边在搜索的过程中顺便标记一下即可。
更优的方法是在 \(k=2\) 的解基础上构造,只要找到一组简单的模式即可。
显然 \(k\ge 4\) 可以转化为 \(k-2\) 问题。
时间复杂度 \(\mathcal O(mk)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
struct Edge { int v,i; };
basic_string <Edge> G[MAXN];
basic_string <int> Z,E[MAXN],U[MAXN];
int n,m,k,h[MAXN];
bool vis[MAXN],in[MAXN];
void add(int d,int u,int v) { for(int i=0;i<d;++i) Z+=i&1?v:u; }
void dfs1(int u) {
for(int &i=h[u];i<(int)G[u].size();++i) if(!vis[G[u][i].i]) {
auto e=G[u][i]; vis[e.i]=1,dfs1(e.v),Z+=e.v;
}
}
void dfs2(int u) {
in[u]=1;
for(auto e:G[u]) if(!vis[e.i]) {
Z+=e.v,vis[e.i]=1;
if(!in[e.v]) dfs2(e.v);
add(k-1,u,e.v);
}
}
void dfs3(int u) {
for(auto e:G[u]) {
if(!h[e.v]) h[e.v]=h[u]+1,dfs3(e.v),E[u]+=e.v;
else if(h[e.v]<h[u]-1) U[u]+=e.v;
}
}
int dfs4(int u,int o) {
for(int x:U[u]) {
if(o==2) add(k-2,x,u),o=1;
else if(o==1) add(k,u,x),o=0;
else add(k,x,u),o=1;
}
for(int v:E[u]) {
int c;
if(h[u]&1) {
if(o&1) add(k,u,v),c=dfs4(v,1);
else add(k-o,v,u),c=dfs4(v,0);
if(c==0) o=1;
else Z+=v,Z+=u,o=2;
} else {
if(!o) c=dfs4(v,1);
else Z+=u,Z+=v,c=dfs4(v,2);
if(c&1) add(k,v,u),o=1;
else add(k-c,u,v),o=0;
}
}
return o;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back({v,i}),G[v].push_back({u,i});
if(k==1) {
vector <int> s;
for(int i=1;i<=n;++i) if(G[i].size()&1) s.push_back(i);
if(s.size()>2||(s.size()&&s[0]>1)) return cout<<"-1\n",0;
dfs1(1),Z+=1,reverse(Z.begin(),Z.end());
} else if(k%2==0) Z={1},dfs2(1);
else Z={1},h[1]=1,dfs3(1),dfs4(1,0);
cout<<Z.size()<<"\n";
for(int x:Z) cout<<x<<" "; cout<<"\n";
return 0;
}
D. [QOJ8564] Three Vectors (2)
显然最优解是对每对 \((x_i,x_j)\) 都把没出现过的取值对用一条语句禁用。
优化考虑本质不同的下标只有 \(8\) 类,对于同类下标只要标记它们相等,不同类下标之间可以用这种方式暴力建边。
两个同类下标可以用两条语句使其相等,不过进一步注意到可以用一条语句确定 \(x_i\ge x_j\),因此对于多个同等价类的元素,只要用 $\ge $ 号连成一个环即可,操作次数 \(n+\mathcal O(1)\)。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5;
int n; vector<array<int,2>>Z;
vector <int> f[8];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
string A,B,C; cin>>n>>A>>B>>C;
for(int i=0;i<n;++i) f[A[i]-'0'+(B[i]-'0')*2+(C[i]-'0')*4].push_back(i+1);
for(int o=0;o<8;++o) if(f[o].size()>1) {
int x=f[o].back();
for(int y:f[o]) Z.push_back({-x,y}),x=y;
}
for(int u=0;u<8;++u) for(int v=u+1;v<8;++v) if(f[u].size()&&f[v].size()) {
int x=f[u][0],y=f[v][0],d=0;
for(int i:{0,1,2}) d|=1<<(2*(u>>i&1)+(v>>i&1));
for(int i:{0,1}) for(int j:{0,1}) if(!(d>>(2*i+j)&1)) Z.push_back({i?-x:x,j?-y:y});
}
cout<<Z.size()<<"\n"; for(auto&o:Z) cout<<o[0]<<" "<<o[1]<<"\n";
return 0;
}
E. [QOJ4802] Ternary Search (2)
假设序列单峰,单组数据求解只要从小到大确定每个数放前缀还是后缀即可,代价就是前缀和后缀中比该元素大的数个数的较小值。
那么对每个前缀,决策肯定是一段时间内放后缀,然后切换成前缀,可以简单二维偏序维护。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,a[MAXN],b[MAXN];
ll f[MAXN],z;
struct BIT {
int tr[MAXN];
void init() { memset(tr,0,sizeof(tr)); }
void add(int x,int v) { for(;x<=n;x+=x&-x) tr[x]+=v; }
int qry(int x) { int s=0; for(;x;x&=x-1) s+=tr[x]; return s; }
int fd(int k) {
int x=0;
for(int i=1<<17;i;i>>=1) if(x+i<=n&&k>tr[x+i]) k-=tr[x+=i];
return x+1;
}
} S,T;
vector <int> d[MAXN];
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],b[i]=i;
sort(b+1,b+n+1,[&](int x,int y){ return a[x]<a[y]; });
for(int i=n;i>=1;--i) {
d[S.fd(2*S.qry(b[i])+1)].push_back(i);
S.add(b[i],1),a[b[i]]=i;
}
for(int i=1;i<=n;d[i++].clear()) {
for(int x:d[i]) T.add(x,-1);
f[i]=f[i-1]+T.qry(a[i]),T.add(a[i],1);
}
S.init(),T.init();
for(int i=1;i<=n;++i) {
d[S.fd(2*S.qry(b[i])+1)].push_back(n-i+1);
S.add(b[i],1),a[b[i]]=n-i+1;
}
for(int i=1;i<=n;++i) {
for(int x:d[i]) T.add(x,-1);
z+=T.qry(a[i]),T.add(a[i],1),cout<<min(z,f[i])<<"\n";
}
return 0;
}
F. [QOJ13329] Counting Cactus (3)
求出每个点集生成环的方案数后做边双联通 - 连通变换。
时间复杂度 \(\mathcal O(n^32^n)\)。
代码:
#include<bits/stdc++.h>
#define pc __builtin_popcount
using namespace std;
const int MAXN=1<<18|5,MOD=998244353,i2=(MOD+1)/2;
void add(int&x,const int&y) { x=x+y>=MOD?x+y-MOD:x+y; }
void sub(int&x,const int&y) { x=x>=y?x-y:x+MOD-y; }
int iv[19];
void fwt(int N,int*F,auto&A) {
memset(A,0,sizeof(A[0])<<N);
for(int s=0;s<(1<<N);++s) A[s][pc(s)]=F[s];
for(int k=1;k<(1<<N);k<<=1) for(int s=0;s<(1<<N);++s) if(s&k) {
for(int i=0;i<=N;++i) add(A[s][i],A[s^k][i]);
}
}
void ifwt(int N,int*F,auto&A) {
for(int k=1;k<(1<<N);k<<=1) for(int s=0;s<(1<<N);++s) if(s&k) {
for(int i=0;i<=N;++i) sub(A[s][i],A[s^k][i]);
}
for(int s=0;s<(1<<N);++s) F[s]=A[s][pc(s)];
}
int a[MAXN][19],b[MAXN][19];
void exp(int n,int *g) {
static int f[19];
memset(f,0,sizeof(f)),f[0]=1;
for(int i=1;i<=n;++i) {
for(int j=1;j<=i;++j) f[i]=(f[i]+1ll*g[j]*f[i-j]%MOD*j)%MOD;
f[i]=1ll*f[i]*iv[i]%MOD;
}
for(int i=0;i<=n;++i) g[i]=f[i];
}
void ln(int n,int *g) {
static int f[19];
memset(f,0,sizeof(f)),f[0]=1;
for(int i=1;i<=n;++i) {
for(int j=1;j<i;++j) f[i]=(f[i]+1ll*f[j]*g[i-j]%MOD*j)%MOD;
f[i]=(g[i]+1ll*(MOD-f[i])*iv[i])%MOD;
}
for(int i=0;i<=n;++i) g[i]=f[i];
}
void mul(int n,int *f,int *g) {
static int h[19];
memset(h,0,sizeof(h));
for(int i=0;i<=n;++i) for(int j=0;j<=i;++j) h[i]=(h[i]+1ll*f[j]*g[i-j])%MOD;
for(int i=0;i<=n;++i) f[i]=h[i];
}
int n,m,e[19];
int no(int s,int i) { return (s>>(i+1)<<i)|(s&((1<<i)-1)); }
void solve(int *f,int z) {
static int g[MAXN];
for(int i=0;i<n;++i) {
for(int s=0;s<(1<<n);++s) if(!(s>>i&1)) {
g[no(s,i)]=(1ll*f[s]*z%MOD*pc(e[i]&s&((1<<i)-1))+f[s|1<<i])%MOD;
}
fwt(n-1,g,a);
for(int s=0;s<(1<<(n-1));++s) exp(n-1,a[s]);
ifwt(n-1,g,a);
for(int s=0;s<(1<<n);++s) if(s>>i&1) f[s]=g[no(s,i)];
}
}
int f[MAXN],dp[MAXN][18];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m,iv[1]=1;
for(int i=2;i<=n;++i) iv[i]=1ll*iv[MOD%i]*(MOD-MOD/i)%MOD;
for(int i=0,u,v;i<m;++i) cin>>u>>v,--u,--v,e[u]|=1<<v,e[v]|=1<<u;
for(int h=n-1;h>1;--h) {
memset(dp,0,sizeof(dp));
for(int i=0;i<h;++i) if(e[h]>>i&1) dp[1<<i][i]=1;
for(int s=0;s<(1<<h);++s) for(int i=0;i<h;++i) if(s>>i&1) {
for(int j=0;j<h;++j) if((e[i]>>j&1)&&!(s>>j&1)) add(dp[s|1<<j][j],dp[s][i]);
}
for(int i=0;i<h;++i) if(e[h]>>i&1) {
for(int s=0;s<(1<<h);++s) if(pc(s)>=2) f[s|1<<h]=(f[s|1<<h]+1ll*i2*dp[s][i])%MOD;
}
}
for(int i=0;i<n;++i) f[1<<i]=1;
solve(f,1);
cout<<f[(1<<n)-1]<<"\n";
return 0;
}
G. [QOJ11693] Short Random Problem (4.5)
枚举直径中点所在边,那么对于两侧子树分别计算 \(f_u(x)\) 表示子树内最大深度为 \(x\) 的概率。
转移用积分和微分实现即可,即 \(\int_0^x f_u=\prod_v \int _{x-1}^x\mathrm{d} t\int _0^t f_v\),具体实现时维护 \(\int_0^x f_u\) 对应的多项式 \(F_u\),操作就是先 \(F_v\gets \int F_v\),然后 \(F_v(x)\gets F_v(x)-F_v(x-1)\),再求 \(\prod F_v\)。
注意到 \(f\) 是分段函数,因为叶子的 \(f_u(x)=[x=0]\),所以转移时 \(f_u\) 应该在每个整数范围 \(x\in [w-1,w]\) 之间都是某个多项式,那么维护所有 \(F_{u,w}(x)=\int_{w-1}^{w-1+x} f_u\) 即可,注意做积分时要继承 \(F_{u,w-1}(1)\) 作为常数项。
最后合并左右两个子树的函数 \(f,g\) 时,假设左子树深度较大,枚举得到答案为 \(\int_{0}^{\infty}\mathrm dx f(x)\int_{x-1}^x\mathrm dyg(y)\int_{x-y}^1 (x+y+z)\mathrm dz\)。
把最后一个式子拆成关于 \(x,y\) 的多项式,分别维护 \(\int_0^yg(y),\int_0^y g(y)y,\int_0^y g(y)y^2\) 即可。
时间复杂度 \(\mathcal O(n^4)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#define sz(a) ((int)a.size())
using namespace std;
const ll MOD=1e9+7,i2=(MOD+1)/2;
typedef vector<ll> poly;
ll iv[105];
poly operator +(const poly&f,const poly&g) {
poly h(max(sz(f),sz(g)));
for(int i=0;i<sz(f);++i) h[i]=f[i];
for(int i=0;i<sz(g);++i) h[i]=(h[i]+g[i])%MOD;
return h;
}
poly operator -(const poly&f,const poly&g) {
poly h(max(sz(f),sz(g)));
for(int i=0;i<sz(f);++i) h[i]=f[i];
for(int i=0;i<sz(g);++i) h[i]=(h[i]+MOD-g[i])%MOD;
return h;
}
poly operator *(const poly&f,const poly&g) {
if(f.empty()||g.empty()) return {};
poly h(sz(f)+sz(g)-1);
for(int i=0;i<sz(f);++i) for(int j=0;j<sz(g);++j) h[i+j]=(h[i+j]+f[i]*g[j])%MOD;
return h;
}
poly Der(const poly&f) {
if(f.size()<=1) return {};
poly g(sz(f)-1);
for(int i=1;i<sz(f);++i) g[i-1]=i*f[i]%MOD;
return g;
}
poly Int(const poly&f) {
poly g(sz(f)+1);
for(int i=0;i<sz(f);++i) g[i+1]=f[i]*iv[i+1]%MOD;
return g;
}
ll S(const poly&f) { return accumulate(f.begin(),f.end(),0ll)%MOD; }
vector <int> G[105];
int n; ll z=0;
poly f[105][105],a[105],b[105];
void calc(int u,int v) {
for(int i=1;i<=n;++i) a[i]=Der(f[u][i]),b[i]=f[v][i];
for(int i=0;i<n;++i) {
z=(z+S(Int(a[i+1]*poly{(i+(1+MOD-3*i*i)*i2)%MOD,(1-3*i+MOD)%MOD,(MOD-i2)*3%MOD}*(b[i+1]-b[i]))))%MOD;
}
for(int i=1;i<=n;++i) {
b[i]=Int(Der(f[v][i])*poly{i-1,1}),b[i][0]=S(b[i-1]);
z=(z+S(Int(a[i]*poly{i,1}*(b[i]-b[i-1]))))%MOD;
}
for(int i=1;i<=n;++i) {
b[i]=Int(Der(f[v][i])*poly{(i-1)*(i-1),2*(i-1),1}),b[i][0]=S(b[i-1]);
z=(z+S(Int(a[i]*(b[i]-b[i-1])))*i2)%MOD;
}
}
void dfs(int u,int fz) {
for(int i=1;i<=n;++i) f[u][i]={1};
for(int v:G[u]) if(v^fz) {
dfs(v,u);
for(int i=1;i<=n;++i) f[v][i]=Int(f[v][i]),f[v][i][0]=S(f[v][i-1]);
for(int i=n;i>=1;--i) f[v][i]=f[v][i]-f[v][i-1];
for(int i=1;i<=n;++i) f[u][i]=f[u][i]*f[v][i];
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n,iv[1]=1;
if(n==2) return cout<<i2<<"\n",0;
for(int i=2;i<=n;++i) iv[i]=(MOD-MOD/i)*iv[MOD%i]%MOD;
for(int i=1,u,v;i<n;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
for(int u=1;u<=n;++u) for(int v:G[u]) if(v>u) {
dfs(u,v),dfs(v,u),calc(u,v),calc(v,u);
}
cout<<z<<"\n";
return 0;
}
H. [QOJ8190] Jaw-Dropping Set (2)
首先取 \(>\lfloor \frac n2\rfloor\) 的所有数构造一个大小为 \(\lceil \frac n2\rceil\) 的解,其次对于每个 \(2\nmid k\) 考虑形如 \(k2^t\) 的所有数最多选一个,那么就证明了这是上界。
我们只要确定枚举 \(k\) 选哪个 \(t\) 即可,显然从大到小每个 \(k\) 尽可能选最小的 \(t\),即所有倍数之中的 \(1+\max_{k\mid i} t_i\)。
可以发现 \(t_k\) 就是最多加入几个因子仍然 \(\le n\),即 \(\log_3(n/k)\),那么只要枚举每种 \(t\) 填给哪些 \(k\) 即可。
时间复杂度 \(\mathcal O(\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
ll S(ll x) { return x=(x+1)/2,x*x; }
void solve() {
ll n,z=0; cin>>n;
for(ll i=0,x=1;x<=n;++i,x*=3) z+=(S(n/x)-S(n/x/3))<<i;
cout<<z<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
*I. [QOJ4881] Hard Problem (7.5)
假设左侧最大值 \(a_p\) 比右侧最大值小 \(\le k\),那么枚举 \(p\),此时区间左边界不能包含左侧第一个 \(>p\) 的位置 \(l\),右边界不能包含右侧第一个 \(> a_p+k\) 的位置 \(r\),并且右半边必须包含右侧首个 \(\ge a_p\) 的位置 \(x\)。
暴力枚举分解线位置,用上面的结论优化枚举范围为 \(\min(x-p,\frac {p+r}2-\frac {l+x}2)\) 级别,然后直接计算答案。
记 \(A=p-l,B=x-p,C=r-x\),那么分析 \(\sum \min( B,A+C)\) 的范围。
建立笛卡尔树,其中 \(A,B\) 是 \(p\) 的左右子树,如果 \(B\le A\) 那么代价不超过 \(p\) 的轻子树大小。
否则 \(A\) 是 \(p\) 的轻子树,而 \(C\) 由笛卡尔树上 \(x\to r\) 的链上每个点的右子树构成,如果链上有一个点的轻子树是左子树,那么 \(B\) 不超过这个点的轻子树大小,否则 \(C\) 不超过这些点的轻子树大小之和。
也就是每个点的轻子树最多被计算 \(k\) 次,复杂度可以接受。
时间复杂度 \(\mathcal O(nk\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=5e5+5,MOD=998244353;
int n,k,a[MAXN],b[MAXN],L[MAXN],R[MAXN],q[MAXN];
ll f[MAXN],z;
void solve() {
cin>>n>>k,q[0]=n+1,z=0;
for(int i=1;i<=n;++i) cin>>a[i];
for(int i=n,h=0;i;R[i]=q[h],q[++h]=i--) for(;h&&a[q[h]]<=a[i];--h);
for(int i=1,h=q[0]=0;i<=n;L[i]=q[h],q[++h]=i++) for(;h&&a[q[h]]<=a[i];--h);
fill(b+1,b+n+1,n+1);
for(int i=n,j,h;i;b[a[i]]=i,--i) {
if((j=min(b[a[i]],R[i]))>n||a[j]>a[i]+k) continue;
for(h=j;h<=n&&a[h]<=a[i]+k;h=R[h]);
for(int x=max(i,(L[i]+j+1)/2);x<min(j,(h+i)/2);++x) {
int l=max(x-i+1,j-x),r=min(x-L[i],h-x-1);
if(l<=r) z=(z+(a[x]+10)*(f[r]-f[l-1]+MOD))%MOD;
}
}
memset(b,0,4*n+4);
for(int i=1,j,h;i<=n;b[a[i]]=i,++i) {
if((j=max(b[a[i]],L[i]))<1||a[j]>a[i]+k) continue;;
for(h=j;h>=1&&a[h]<=a[i]+k;h=L[h]);
for(int x=max(j,(h+i+1)/2);x<min(i,(R[i]+L[i])/2);++x) {
int l=max(x-L[i]+1,i-x),r=min(x-h,R[i]-x-1);
if(l<=r) z=(z+(a[x]+10)*(f[r]-f[l-1]+MOD))%MOD;
}
}
cout<<z<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
f[1]=3240,f[2]=3081,f[3]=2841,f[4]=343;
for(int i=5;i<MAXN;++i) f[i]=(f[i-1]*223+f[i-2]*229+f[i-3]*f[i-4]%MOD*239+17)%MOD;
for(int i=2;i<MAXN;++i) f[i]=(f[i-1]+f[i])%MOD;
int _; cin>>_;
while(_--) solve();
return 0;
}
J. [QOJ4219] Insects (5.5)
根据 Hall 定理,我们找一条左上到右下的分界线,最大化分界线上方的黑点加下方的白点。
把所有点中心对称,看成最大化分界线下方黑点减白点个数,如果只有单组数据那么直接 dp \(f_{i,j}\) 表示第 \(i\) 行选了前 \(j\) 列的最优解。
转移时维护差分 \(d\),加入白点时相当于 \(d_y\gets d_y+1\),加入黑点就是匹配左侧最后一个 \(d_y>0\) 并令 \(d_y\gets d_y-1\) ,可以 \(\mathcal O((n+m)\log n)\) 维护。
然后考虑刻画解的结构,首先我们发现对于某个时刻的所有权值取到最大值 \(v\) 的折线,每个位置越低越好,因为这样加入新的白点更可能不落在折线下方。
不过我们无法比较多条相交折线的优劣,不妨猜测必定存在一条在所有折线下方的最优解,证明考虑两条相交折线 \(f,g\),找到最左侧交点 \(x\),设它们在 \(\le x,>x\) 的部分和分别是 \(f_l,f_r,g_l,g_r\),则 \(f_l+f_r=g_l+g_r=v,f_l+g_r\le v,g_l+f_r\le v\),那么 \(f_l+g_r=g_l+f_r=v\),也就是我们把这两条折线所有交点之间的部分取出来,每段都可以任意选择填 \(f\) 还是 \(g\) 的部分,所以一个能偏序所有可能答案的最优解一定存在。
进一步不难证明加入白点对最优解的影响一定是缩小折线范围,这是因为新的白点如果在最优解上方则答案不变,否则答案一定变小,且当前折线就是一组取到最大值的解,则最优解一定在当前折线下方。
所以我们可以整体二分,求出 $mid $ 时刻的最优解,并把折线内外的部分分成两个点集分别解决,注意到下部点集中的点一定被上部点集的答案包含,所以只要对这些点按插入时间维护二维偏序即可。
时间复杂度 \(\mathcal O((n+m)\log V\log m)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,V=1e5+1;
struct info { int x,y,i; };
int n,m,ans[MAXN];
struct BIT {
int tr[MAXN],st[MAXN*2],tp;
void add(int x,int v) { for(st[++tp]=x;x<=V;x+=x&-x) tr[x]+=v; }
int qs(int x) { int s=0; for(;x;x&=x-1) s+=tr[x]; return s; }
int fd(int k) {
int x=0;
for(int i=1<<16;i;i>>=1) if(x+i<=V&&tr[x+i]<k) k-=tr[x+=i];
return x+1;
}
void init() { while(tp) for(int x=st[tp--];x<=V;x+=x&-x) tr[x]=0; }
} B,F;
void solve(int L,int R,vector<info>&Q) {
int p=(L+R)>>1,z=0;
vector <info> dp,QL,QR;
for(auto o:Q) {
if(!o.i) {
int c=F.qs(o.y);
if(c) F.add(c=F.fd(c),-1);
else ++z;
dp.push_back({c,o.y,o.x});
} else if(o.i<=p) F.add(o.y,1);
}
F.init(),ans[p]=B.qs(p)+z;
for(int i=(int)Q.size()-1,k=(int)dp.size()-1,j,h=0;~i;i=j) {
for(;~k&&dp[k].i==Q[i].x;--k) if(dp[k].x<=h&&h<dp[k].y) h=dp[k].y;
for(j=i;~j&&Q[j].x==Q[i].x;--j) (Q[j].y<=h?QR:QL).push_back(Q[j]);
}
reverse(QL.begin(),QL.end()),reverse(QR.begin(),QR.end());
if(p<R) solve(p+1,R,QR);
else for(auto o:QR) B.add(max(1,o.i),o.i?-1:1);
if(L<p) solve(L,p-1,QL);
else for(auto o:QL) B.add(max(1,o.i),o.i?-1:1);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
vector <info> A;
cin>>n;
for(int i=1,x,y;i<=n;++i) cin>>x>>y,A.push_back({V-x,V-y,0});
cin>>m;
for(int i=1,x,y;i<=m;++i) cin>>x>>y,A.push_back({V-x,V-y,i});
sort(A.begin(),A.end(),[&](auto i,auto j){ return i.x^j.x?i.x<j.x:i.i>j.i; });
solve(1,m,A);
for(int i=1;i<=m;++i) cout<<n-ans[i]<<" \n"[i==m];
return 0;
}
*K. [QOJ12152] DNA Subsequences (7.5)
答案可以看成选出 \(k\) 个不相交子区间,调整使得每个子区间都包含 \(\texttt{A,C,G,T}\) 的最小代价。
显然每个区间的代价函数满足四边形不等式,从而答案具有凸性,可以通过 wqs 二分求单点值。
本题最大的特点是凸壳上的点值域 \(\le n\),而凸壳的斜率递增,所以斜率不同的连续段只有 \(\mathcal O(\sqrt n)\) 个,即凸壳上拐点数只有 \(\mathcal O(\sqrt n)\) 个。
尝试每次确定凸壳上一个新的点,这是经典问题,从两端点 \((L,R)\) 出发,以 \(R-L\) 的斜率求该凸壳的切线即可得到一个凸壳上 \(L,R\) 之间的拐点,如果得到的点在 \(L,R\) 连线上说明内部没有拐点,否则可以递归。
时间复杂度 \(\mathcal O(n\sqrt n)\)。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2e5+5;
int n,f[MAXN]; array<int,4>b[MAXN];
struct info { int x,y; ll z; } g[MAXN];
bool operator<(const info&u,const info&v) { return u.z^v.z?u.z<v.z:u.x<v.x; }
void cdq(int L,int R) {
if(L>=R) return ;
int x=R-L,y=f[R]-f[L];
for(int i=1;i<=n;++i) {
g[i]=g[i-1];
for(int c:{0,1,2,3}) {
int j=min(i-4,b[i][c]-1);
if(j>=0) g[i]=min(g[i],{g[j].x+1,g[j].y+3-c,g[j].z-y+x*(3-c)});
}
}
int p=g[n].x; f[p]=g[n].y;
if(1ll*y*(R-p)==1ll*x*(f[R]-f[p])) {
for(int k=L;k<=R;++k) f[k]=f[L]+1ll*y*(k-L)/x;
} else cdq(L,p),cdq(p,R);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) {
char c; cin>>c;
b[i]=b[i-1],b[i][(c>'A')+(c>'C')+(c>'G')]=i;
}
for(int i=1;i<=n;++i) sort(b[i].begin(),b[i].end(),greater<>());
f[0]=0,f[n/4+1]=2*n;
cdq(0,n/4+1);
for(int i=1;i<=n;++i) cout<<(i<=n/4?f[i]:-1)<<" \n"[i==n];
return 0;
}
L. [QOJ4884] Battleship: New Rules (4.5)
考虑把每个矩形向右向下拓展一个变成长或宽为 \(2\) 的矩形,并把总大小变成 \(n+1\),则最大面积就是 \(\frac{(n+1)^2}{2}-k\)。
此时如果 \(n\) 是奇数则必定会被完整密铺,否则只会剩下一个空格子,那么合法的 \(2\times 2\) 就是这个格子为右下角的区域。
考虑如何求该格子的位置,显然需要二分,考虑一个矩形内部的大小为奇数时则包含这个空格子,否则不包含。
一个矩形的内部大小就是减掉所有和外界有交的矩形之后剩余的大小,首先算四个角上的点,然后对于矩形的右边界,如果是横向的 \(2\times k\) 穿过,则内部大小必为偶数,可以忽略,而纵向的 \(k\times 2\) 只要计算右边界的点所在矩形是否在经过外侧,其他四边界同理,记忆化后每次只会询问边界,算上初始右下边界的次数,总询问次数 \(5n+\mathcal O(\log n)\)。
时间复杂度 \(\mathcal O(n^2)\)
代码:
#include<bits/stdc++.h>
using namespace std;
int n,a[1005][1005];
int f(int x,int y) {
if(x<1||y<1||x>n||y>n) return 0;
if(~a[x][y]) return a[x][y];
return cout<<"? "<<x<<" "<<y<<endl,cin>>a[x][y],a[x][y];
}
bool chk(int lx,int rx,int ly,int ry) {
bool o=(rx-lx+1)*(ry-ly+1)%2;
for(int x:{lx-1,rx}) for(int i=ly+1,u=f(x,ly),v;i<ry;++i,u=v) v=f(x,i),o^=u||v;
for(int y:{ly-1,ry}) for(int i=lx+1,u=f(lx,y),v;i<rx;++i,u=v) v=f(i,y),o^=u||v;
o^=f(lx,ry)||f(lx-1,ry)||f(lx-1,ry-1);
o^=f(lx,ly-1)||f(lx-1,ly-1)||f(lx-1,ly);
o^=f(rx-1,ly-1)||f(rx,ly-1)||f(rx,ly);
o^=f(rx-1,ry)||f(rx,ry)||f(rx,ry-1);
return o;
}
void solve() {
cin>>n;
if(n&1) return cout<<"! -1 -1"<<endl,void();
for(int i=1;i<=n;++i) memset(a[i],-1,sizeof(a[i]));
int lx=1,rx=n+1,ly=1,ry=n+1;
for(int m;rx-lx>1&&ry-ly>1;) {
if(rx-lx>ry-ly) m=(lx+rx)>>1,(chk(lx,m,ly,ry)?rx=m:lx=m+1);
else m=(ly+ry)>>1,(chk(lx,rx,ly,m)?ry=m:ly=m+1);
}
for(int x=max(2,lx);x<=min(n,rx);++x) for(int y=max(2,ly);y<=min(n,ry);++y) {
if(!f(x,y)&&!f(x-1,y)&&!f(x,y-1)&&!f(x-1,y-1)) return cout<<"! "<<x-1<<" "<<y-1<<endl,void();
}
cout<<"! -1 -1"<<endl;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _,o; cin>>_;
while(_--) solve(),cin>>o;
return 0;
}
M. [QOJ12160] Projects (5)
把最大值 \(V\) 设成线性规划变量,\(x_{i,j}\le a_{i,j},\sum x_{i,j}+\sum 2(a_{j,i}-x_{j,i})\le V,\min V\)。
对偶得到 \(2z_j-z_i-y_{i,j}\le 0,\sum z_i\le 1,\max \sum a_{i,j}(2z_j-y_{i,j})\)。
显然 \(y_{i,j}=\max(0,2z_j-z_i)\),从而 \(\sum a_{i,j}(2z_j-y_{i,j})=\sum a_{i,j}\min(z_i,2z_j)\)。
因此只要确定 \(z\) 数组即可,容易想到最优解中 \(z\) 的元素之间的比值应该都是 \(2\) 的幂,且应该是连续的,也就是按比例为 \(0,2^0,2^1,\dots, 2^k\) 连续分配。
那么我们按权重从高到低确定点集,每次把已确定点的权重翻倍,dp 记录集合 \(s\) 和当前权重之和 \(x\),转移 \(f_{s,x}\to f_{s+t,2x+|t|}\),由于每次转移的 \(t\) 非空,所以 \(x<2^{|s|}\),暴力枚举子集转移即可。
时间复杂度 \(\mathcal O(3^nn^+4^n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
#define pc __builtin_popcount
using namespace std;
const int MAXN=1<<13|5,MAXS=1.6e6+5;
int n,e,q,h[MAXN];
ll f[MAXS],L[MAXN],R[MAXN],a[13][13];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>e>>q,memset(f,-0x3f,sizeof(f)),f[0]=0;
for(int s=0;s<(1<<n);++s) h[s+1]=h[s]+(1<<pc(s));
for(int i=1,u,v,w;i<=e;++i) cin>>u>>v>>w,a[u-1][v-1]+=w;
for(int s=0;s<(1<<n);++s) {
int m=1<<pc(s); ll *g=f+h[s],x=0,y=1;
for(int i=1;i<m;++i) if(g[i]>0&&x*i<g[i]*y) x=g[i],y=i;
ll d=__gcd(x,y); L[s]=x/d,R[s]=y/d;
for(int t=s|(s+1);t<(1<<n);t=(t+1)|s) {
ll z=0,*w=f+h[t]+pc(t-s);
for(int i=0;i<n;++i) if(t>>i&1) for(int j=0;j<n;++j) if(t>>j&1) {
if((t-s)>>i&1) z+=a[i][j];
else if((t-s)>>j&1) z+=a[i][j]*2;
}
for(int i=0;i<m;++i) w[2*i]=max(w[2*i],g[i]*2+z);
}
}
for(int i=1;i<(1<<n);i<<=1) for(int s=0;s<(1<<n);++s) if(s&i) {
if(L[s]*R[s^i]<L[s^i]*R[s]) L[s]=L[s^i],R[s]=R[s^i];
}
for(string S;q--;) {
cin>>S; int s=0;
for(int i=0;i<n;++i) s|=(S[i]-'0')<<i;
cout<<L[s]<<"/"<<R[s]<<"\n";
}
return 0;
}
*N. [QOJ17155] Fix the Coloring (7)
可以想到要 2-SAT,但是没有一个较好的方式区分团和独立集中的点。
一个观察是枚举团内的某个点 \(x\),那么此时一个红点在团内当且仅当其和 \(x\) 有边。
此时限制是如果 \((u,v)\) 有边且 \((u,x),(v,x)\) 不同时有边则不能同时染红,反之 \((u,v)\) 无边但 \((u,x),(v,x)\) 均有边也不能。
此时 2-SAT 限制数 \(m+\deg(x)^2\),总边数是 \(nm+n^2\) 级别的,直接 tarjan 即可。
时间复杂度 \(\mathcal O(nm+n^2)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3005,MAXM=MAXN*2;
int n,m,q,L[MAXN],R[MAXN],dfn[MAXM],low[MAXM],st[MAXM],tp,bl[MAXM],dc,sc;
bitset <MAXN> E[MAXN],ins;
basic_string <int> G[MAXM];
void link(int x,int y) { G[x]+=y; }
void tarjan(int u) {
dfn[u]=low[u]=++dc,ins[st[++tp]=u]=1;
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],dfn[v]);
}
if(low[u]==dfn[u]) for(++sc;ins[u];ins[st[tp--]]=0) bl[st[tp]]=sc;
}
void solve() {
string s; cin>>n>>m>>s;
for(int i=1;i<=n;++i) E[i].reset();
for(int i=1;i<=m;++i) cin>>L[i]>>R[i],E[L[i]][R[i]]=E[R[i]][L[i]]=1;
for(int x=1;x<=n;++x) {
dc=sc=0;
for(int i=1;i<=2*n;++i) G[i].clear(),bl[i]=dfn[i]=low[i]=0;
link(x,x+n);
vector <int> Q;
for(int i=1;i<=m;++i) {
if(L[i]==x||R[i]==x) Q.push_back(L[i]+R[i]-x);
else if(E[L[i]][x]+E[R[i]][x]<2) link(L[i]+n,R[i]),link(R[i]+n,L[i]);
if(s[L[i]-1]==s[R[i]-1]) link(L[i],R[i]+n),link(R[i],L[i]+n);
}
for(int u:Q) for(int v:Q) if(u<v&&!E[u][v]) link(u+n,v),link(v+n,u);
for(int i=1;i<=2*n;++i) if(!dfn[i]) tarjan(i);
for(int i=1;i<=n;++i) if(bl[i]==bl[i+n]) goto fl;
for(int i=1;i<=n;++i) if(bl[i+n]<bl[i]) s[i-1]='R';
return cout<<"YES\n"<<s<<"\n",void(); fl:;
}
cout<<"NO\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
O. [QOJ6545] Connect the Dots (4)
连边构成三角剖分状物,那么相邻的边不影响其他边,可以连的全连上。
考虑先连接距离为二的点对,不断进行次操作,停止时的环一定时两种颜色间隔出现。
如果 \(m\le 2\),可以证明此时随便找一个点向外连满菊花就是最优的,否则必定有一种颜色被删空了,在这种颜色还剩一个点的时候从这个点向外连满菊花即可取到最大值。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
int n,m,a[MAXN],L[MAXN],R[MAXN],c[MAXN];
bool in[MAXN];
bool ok(int x) { return a[L[x]]!=a[R[x]]; }
void solve() {
cin>>n>>m,fill(c,c+m+1,0);
for(int i=1;i<=n;++i) cin>>a[i],L[i]=(i+n-2)%n+1,R[i]=i%n+1,++c[a[i]],in[i]=1;
if(n==2) {
if(a[1]==a[2]) cout<<"0\n";
else cout<<"1\n1 2\n";
return ;
}
queue <int> Q; vector<array<int,2>> Z;
for(int i=1;i<=n;++i) {
if(ok(i)) Q.push(i);
if(a[i]!=a[R[i]]) Z.push_back({i,R[i]});
}
for(int i=1;i<=n;++i) if(c[a[i]]==1) queue<int>().swap(Q);
for(int t=n;Q.size()&&t>3;) {
int x=Q.front(); Q.pop();
if(!in[x]||!ok(x)) continue;
Z.push_back({L[x],R[x]}),--t;
R[L[x]]=R[x],L[R[x]]=L[x],in[x]=0;
if(--c[a[x]]==1) break;
for(int y:{L[x],R[x]}) if(ok(y)) Q.push(y);
}
int x=0;
vector <int> b;
for(int i=1;i<=n;++i) if(in[i]) b.push_back(i),x=(c[a[i]]==1?i:x);
if(!x) x=b[0];
for(int y:b) if(y!=L[x]&&y!=x&&y!=R[x]&&a[y]!=a[x]) Z.push_back({x,y});
cout<<Z.size()<<"\n";
for(auto o:Z) cout<<o[0]<<" "<<o[1]<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
P. [QOJ6104] Building Bombing (3.5)
每次 dp \(k\to k+1\),转移就是 \(f_{k+1,i}\gets g_{k,j}+\sum_{i<t<j}[a_t>a_i]\),按值域从大到小插入下标,转移代价变成 \(i+1\sim j-1\) 中已插入下标个数,容易线段树。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,inf=1e9,N=1<<17;
int n,m;
struct info { int s,z; } tr[1<<18|5],O={0,inf};
info operator +(const info&u,const info&v) { return {u.s+v.s,min(u.z,u.s+v.z)}; }
void ins(int x,int z) {
for(tr[x+=N]={1,z},x>>=1;x;x>>=1) tr[x]=tr[x<<1]+tr[x<<1|1];
}
int qry(int x) {
info s=O;
for(x+=N;x>1;x>>=1) if(~x&1) s=s+tr[x^1];
return s.z;
}
int o,k,a[MAXN],b[MAXN],f[MAXN],g[MAXN];
vector <int> p[MAXN];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>o>>k;
for(int i=1;i<=n;++i) cin>>a[i],b[i]=a[i];
sort(b+1,b+n+1),m=unique(b+1,b+n+1)-b-1;
for(int i=1;i<=n;++i) p[a[i]=lower_bound(b+1,b+m+1,a[i])-b].push_back(i);
memset(f,0x3f,sizeof(f));
for(int t=1;t<=k;++t) {
fill(tr,tr+(N<<1),O);
if(t==1) ins(n+1,0);
for(int v=m;v>=1;--v) {
for(int x:p[v]) g[x]=qry(x);
for(int x:p[v]) ins(x,f[x]),f[x]=g[x];
}
}
if(f[o]>n) return cout<<"-1\n",0;
for(int i=1;i<o;++i) f[o]+=a[i]>=a[o];
cout<<f[o]<<"\n";
return 0;
}
Q. [QOJ4788] Gravity (2)
按落地顺序处理连通块,即找当前距离已固定格子最近的连通块处理,用堆维护。
时间复杂度 \(\mathcal O(nm\log nm)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005,dx[]={0,0,1,-1},dy[]={-1,1,0,0};
int n,m;
char s[MAXN][MAXN],t[MAXN][MAXN];
vector <array<short,2>> Q[MAXN],h;
void up(short x,short y,int z) {
for(short i=x-1;i;--i) if(s[i][y]!='.') {
if(s[i][y]=='#') Q[x-1-i+z].push_back({i,y});
return ;
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) cin>>s[i][j],t[i][j]='.';
for(int i=1;i<=m;++i) up(n+1,i,0);
for(int d=0;d<=n;++d) while(Q[d].size()) {
short x=Q[d].back()[0],y=Q[d].back()[1]; Q[d].pop_back();
if(s[x][y]!='#') continue;
h={{x,y}},s[x][y]='@';
for(int i=0;i<(int)h.size();++i) {
short u=h[i][0],v=h[i][1],l,r;
for(int o:{0,1,2,3}) if(s[l=u+dx[o]][r=v+dy[o]]=='#') s[l][r]='@',h.push_back({l,r});
}
for(auto&o:h) up(o[0],o[1],d),t[o[0]+d][o[1]]='#';
}
for(int i=1;i<=n;++i,cout<<"\n") for(int j=1;j<=m;++j) cout<<t[i][j];
return 0;
}
*R. [QOJ2626] Kilk Not (8)
先二分,考虑如何判定答案,注意到本题的限制条件较松,不妨猜测只要 \(0,1\) 个数都不少于需要使用的最小值即可。
考虑证明,构造 \(0\) 个数最少和最多的两个串 \(L,R\),定义 \(S_k=R[1,k]+L[k+1,n]\),则 \(c_0(S_{k+1})-c_0(S_k)\in [-1,1]\),根据介值定理必定经过 \(a\),我们找到一个 \(c_0(S_k)=a\) 且 \(R_{k}\ne L_{k+1}\) 就满足条件了。
找到最后一个 \(c_0(S_k)=a-1\) 的位置,则 \(c_0(S_{k+1})\sim c_0(S_n)\ge a\),首先 \(c_0(S_{k+1})=a\),则 \(R_{k+1}=0,L_{k+1}=1\),如果 \(L_{k+2}=1\) 那么 \(S_{k+1}\) 就是合法解。
所以 \(L_{k+2}=0\),由于 \(c_0(S_{k+2})\ge a\) 所以 \(R_{k+2}=0,c_0(S_{k+2})=a\),为了让 \(S_{k+2}\) 不合法则 \(L_{k+3}=0\),那么不断归纳最终我们能得到 \(c_0(S_{k+1})\sim c_0(S_n)\) 均等于 \(a\),此时直接取 \(S_n=R\) 即可。
计算 \(c_0(L),c_0(R)\) 直接 dp 记录 \(f_{i,0/1}\) 表示最后一个连续段在 \(i\) 终止且颜色为 \(0/1\),单调队列优化即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2.5e5+5,inf=1e9;
struct MQ {
int t[MAXN],w[MAXN],l,r;
void ins(int x,int y) {
while(l<=r&&w[r]<=y) --r;
t[++r]=x,w[r]=y;
}
void qry(int h,int&x,int&y) {
while(l<=r&&t[l]<h) ++l;
if(l<=r) x=t[l],y=w[l];
else x=0,y=-inf;
}
} X,Y;
int n,A,B,f[MAXN][2],g[MAXN][2];
char s[MAXN],t[2][MAXN];
int chk(int k,int c) {
X.l=Y.l=1,X.r=Y.r=0;
for(int i=1,e[2]={0,0};i<=n;++i) {
X.ins(i-1,f[i-1][!c]-i+1),Y.ins(i-1,f[i-1][c]);
if(s[i]!='?') e[s[i]-'0']=i;
X.qry(max(e[!c],i-k),g[i][c],f[i][c]),f[i][c]+=i;
Y.qry(max(e[c],i-k),g[i][!c],f[i][!c]);
}
return max(f[n][0],f[n][1]);
}
void solve() {
cin>>n>>A>>B;
for(int i=1;i<=n;++i) cin>>s[i],A+=s[i]=='0';
int z=n;
for(int k=1<<__lg(n);k;k>>=1) if(z>k&&n-chk(z-k,1)<=A&&A<=chk(z-k,0)) z-=k;
cout<<z<<"\n";
for(int c:{0,1}) {
chk(z,c);
for(int i=n,o=f[n][1]>f[n][0];i;i=g[i][o],o^=1) {
fill(t[c]+g[i][o]+1,t[c]+i+1,o+'0');
}
if(count(t[c]+1,t[c]+n+1,'0')==A) {
for(int j=1;j<=n;++j) cout<<t[c][j];
return cout<<"\n",void();
}
}
int c=count(t[1]+1,t[1]+n+1,'0');
for(int i=1;i<n;++i) {
c+=t[1][i]-t[0][i];
if(c==A&&t[0][i]!=t[1][i+1]) {
for(int j=1;j<=n;++j) cout<<t[j>i][j];
return cout<<"\n",void();
}
}
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
*S. [QOJ6546] Greedy Bipartite Matching (7)
考虑把二分图匹配转成点覆盖问题,对 \(w\le v\) 的最优匹配求出一组对应的最小点覆盖 \(C\)。
我们证明对于 \(w\le v\) 的边,只保留两端点不全属于 \(C\) 的,以及 \(w=v+1\) 的边中两端点均不属于 \(C\) 的,可以证明此时取出的最大匹配就是所求,复杂度不超过 \(\mathcal O(\sum m\sqrt{m_i})\le \mathcal O(m\sqrt{mq})\)。
时间复杂度 \(\mathcal O(m\sqrt{mq})\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
int n,m,q,f[MAXN],Q[MAXN],d[MAXN];
vector <int> G[MAXN];
bool in[MAXN],vs[MAXN];
bool bfs() {
int l=1,r=0,o=0; memset(d,-1,sizeof(d));
for(int i=1;i<=n;vs[i++]=0) if(!f[i]) d[i]=0,Q[++r]=i;
for(;l<=r;++l) {
int u=Q[l];
for(int v:G[u]) if(v!=f[u]) {
if(!f[v]) o=1;
else if(d[f[v]]<0) d[f[v]]=d[v]=d[u]+1,Q[++r]=f[v];
}
}
return o;
}
bool dfs(int u) {
if(vs[u]) return 0; vs[u]=1;
for(int v:G[u]) if(!f[v]||(d[v]==d[u]+1&&dfs(f[v]))) return f[v]=u,f[u]=v,1;
return 0;
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>q;
for(int z=0;q--;cout<<z<<" \n"[!q]) {
cin>>m;
for(int u,v;m--;) {
cin>>u>>v,v+=n;
if(!in[u]&&!in[v]) G[u].push_back(v);
}
while(bfs()) for(int i=1;i<=n;++i) if(!d[i]) z+=dfs(i);
for(int i=1;i<=2*n;++i) in[i]|=(d[i]<0)^(i>n);
for(int u=1;u<=n;++u) if(in[u]) {
vector <int> E;
for(int v:G[u]) if(!in[v]) E.push_back(v);
G[u].swap(E);
}
}
return 0;
}
T. [QOJ8004] Bit Component (3)
考虑 \(n=2^k-1\to n=2^{k+1}-1\),先求出 \(n=2^k-1\) 的解 \(S\),然后 \([2^k,2^{k+1})\) 倒过来一份接在后面,即 \(S_{2^k+i}=S_{2^{k}-1-i}+2^k\),最后插入一个 \(2^k\) 即可。
对于一般的 \(n\),记 \(k=\log_2n-1\),那么第 \(k+1\) 位需要一个第 \(k\) 位连通,并且只有 \(3\times 2^k\) 则无法和低位连通,可以证明我们要求 \(n\ge 3\times 2^k+1\)。
此时把 \(2^{k+1}\sim 3\times 2^k-1\) 放在 \(3\times 2^k+1,3\times 2^k\) 中间,内部使用 \(2^{k}-1\) 大小的构造,前面放 \([1,2^{k+1})\) 的构造,但是把 \(2^k\) 放到中间 \(2^{k+1}-1\) 旁边的位置上,这样末尾就能和 \(3\times 2^k+1\) 联通了。
大于 \(3\times 2^k+2\) 的 \(x\) 随便放到 \(x-2^{k+1}\) 的位置旁边即可。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5;
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int n,h; cin>>n,h=__lg(n)-1;
if(n==1) return cout<<"YES\n1\n",0;
if(n==3) return cout<<"YES\n1 3 2\n",0;
if(n<=(3<<h)) return cout<<"NO\n",0;
vector <int> a{1};
for(int k=1;k<=h;++k) {
for(int i=(1<<k)-2;~i;--i) a.push_back(a[i]|1<<k);
if(k<h) a.push_back(1<<k);
}
auto mx=max_element(a.begin(),a.end());
if((*(mx-1))&1) a.insert(mx+1,1<<h);
else a.insert(mx,1<<h);
a.push_back(3<<h|1);
for(int i=0;i<(1<<h)-1;++i) a.push_back(a[i]|2<<h);
a.push_back(3<<h),a.push_back(2<<h);
cout<<"YES\n";
for(int i:a) {
cout<<i<<" ";
if((i>>h)==1&&(1<<h)+1<i&&i+(2<<h)<=n) cout<<i+(2<<h)<<" ";
}
cout<<"\n";
return 0;
}
U. [QOJ8185] Emerging Tree (3)
考虑倒过来维护,删掉边 \(u\to v\) 则要求 \(v\) 是 \([1,n]\) 的前缀或后缀,然后剩下的两个连通块独立。
此时子树结构被影响的就是 \(u\) 的所有祖先,也就是这些子树都应该是当前连通块的后缀。
考虑下一条在 \(u\) 的连通块中的边 \(x\to y\) 有什么限制,如果 \(y\) 是 \(u\) 的祖先或后代,那么依然可以让分出的这一部分是当前连通块的后缀,否则必定要求 \(\mathrm{LCA}(u,y)=rt\),才能让 \(x\to y\) 分出的这一部分是当前连通块的前缀。
按上述过程模拟即可,可以提前正着扫一遍用并查集维护每条边当前所在连通块的根和大小。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e6+5;
int n,dsu[MAXN],sz[MAXN],a[MAXN],b[MAXN],c[MAXN],d[MAXN];
int L[MAXN],R[MAXN],f[MAXN],g[MAXN],dfn[MAXN],efn[MAXN],dc;
vector <int> G[MAXN];
int find(int x) { return dsu[x]^x?dsu[x]=find(dsu[x]):x; }
void dfs(int u) { dfn[u]=++dc; for(int v:G[u]) dfs(v); efn[u]=dc; }
bool in(int u,int v) { return dfn[u]<=dfn[v]&&dfn[v]<=efn[u]; }
int son(int u,int v) {
return *--upper_bound(G[u].begin(),G[u].end(),v,[&](int x,int y){ return dfn[x]<dfn[y]; });
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n;
for(int i=1;i<=n;++i) dsu[i]=i,sz[i]=1;
for(int i=1;i<n;++i) {
cin>>a[i]>>b[i],c[i]=find(a[i]),d[i]=sz[b[i]];
G[a[i]].push_back(b[i]),dsu[b[i]]=a[i],sz[c[i]]+=d[i];
}
int rt=find(1);
dfs(rt),L[rt]=1,R[rt]=n,f[rt]=g[rt]=rt;
for(int i=n-1;i>=1;--i) {
int x=c[i],u=a[i],v=b[i];
bool of=f[x]!=x&&(in(v,f[x])||in(f[x],v)),og=g[x]!=x&&(in(v,g[x])||in(g[x],v));
if(of||(!og&&f[x]==x)) {
L[v]=R[x]-d[i]+1,R[v]=R[x],f[v]=(in(v,f[x])?f[x]:v),g[v]=v,R[x]-=d[i],f[x]=u;
} else if(og||g[x]==x) {
L[v]=L[x],R[v]=L[x]+d[i]-1,g[v]=(in(v,g[x])?g[x]:v),f[v]=v,L[x]+=d[i],g[x]=u;
} else return cout<<"No\n",0;
if(x!=f[x]&&x!=g[x]&&son(x,f[x])==son(x,g[x])) return cout<<"No\n",0;
}
cout<<"Yes\n";
for(int i=1;i<=n;++i) cout<<L[i]<<" \n"[i==n];
return 0;
}
V. [QOJ2207] Edit (4.5)
容易发现 \(3\) 操作是 \(1,2\) 操作的逆操作,因此我们只要在两棵树上找一对边集,使得按照其祖先后代关系建树后相等,用 \(C_1,C_2\) 的代价删除两棵树上剩余的边,用 \(C_3\) 的代价调整边集之间边的权值。
考虑如何刻画边集,对于一对匹配边 \((x,fa_x),(y,fa_y)\),我们要在 \(x,y\) 的子树中选一些互相没有祖先后代的边匹配起来。
在第二棵树上直接按 dfs 序搜索,每次匹配一条边都是删掉一个子树,可以发现剩下还能选的边都是第一棵树上 dfs 序的一个区间。
那么 \(f_{u,l,r}\) 表示 \(u\) 子树匹配第一棵树上 dfn 序被 \([l,r]\) 包含的子树,转移时从所有儿子处合并 \([l,r]\) 即可。
时间复杂度 \(\mathcal O(n^2m)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=2005;
void chk(ll&x,const ll&y) { x=y<x?y:x; }
int n,m,a[55],b[MAXN],L[55],R[55],dc,r1,r2;
ll f[MAXN][55][55],h[55],C1,C2,C3,S;
ll w(int x,int y) { return C3*abs(a[x]-b[y])-C2*a[x]-C1*b[y]; }
vector <int> E[55],G[MAXN];
void dfs1(int u) { L[u]=++dc; for(int v:E[u]) dfs1(v); R[u]=dc; }
void dfs2(int u) {
for(int v:G[u]) {
dfs2(v);
for(int l=2;l<=n;++l) for(int r=n;r>=l;--r) for(int k=l;k<=r;++k) chk(f[u][l][r],f[u][l][k-1]+f[v][k][r]);
}
if(u==r2) return ;
for(int x=1;x<=n;++x) h[x]=w(x,u)+f[u][L[x]+1][R[x]];
for(int x=1;x<=n;++x) for(int l=2;l<=L[x];++l) for(int r=R[x];r<=n;++r) chk(f[u][l][r],h[x]);
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>C1>>C2>>C3>>n,fill(a+1,a+n+1,-1);
for(int i=1,k;i<=n;++i) {
cin>>k,E[i].resize(k);
for(int&x:E[i]) cin>>x,cin>>a[x],S+=a[x]*C2;
}
cin>>m,fill(b+1,b+m+1,-1);
for(int i=1,k;i<=m;++i) {
cin>>k,G[i].resize(k);
for(int&x:G[i]) cin>>x,cin>>b[x],S+=b[x]*C1;
}
r1=find(a+1,a+n+1,-1)-a,r2=find(b+1,b+m+1,-1)-b;
dfs1(r1),dfs2(r2);
cout<<S+f[r2][2][n]<<"\n";
return 0;
}
W. [QOJ8003] Anti-Plagiarism (3)
\(f_{u,v}\) 表示两棵树的 \(u,v\) 子树能否匹配,注意 \(v\) 可能是子树补。
转移考虑从两棵树的儿子处进行完美匹配,由于 \(v\) 上要考虑子树补,所以要求出每个点能否不被最大匹配经过,残量网络上搜索一下即可,复杂度不超过 \(\mathcal O(nm^{1.5})\le \mathcal O((nm)^{1.25})\) 因为 \(m^{0.5}\le (nm)^{0.25}\)。
时间复杂度 \(\mathcal O((nm)^{1.25})\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2e5+5,MAXE=2e7+5,inf=1e9;
struct Edge { int v,f,e; } G[MAXE];
int S,T,ec=1,h[MAXN],c[MAXN],d[MAXN],Q[MAXN];
void init() { ec=1,memset(h,0,T*4+4); }
void adde(int u,int v,int w) { G[++ec]={v,w,h[u]},h[u]=ec; }
void link(int u,int v,int w) { adde(u,v,w),adde(v,u,0); }
bool bfs() {
memcpy(c,h,T*4+4),memset(d,-1,T*4+4),Q[1]=S,d[S]=0;
for(int l=1,r=1,u;l<=r;++l) for(int i=h[u=Q[l]];i;i=G[i].e) if(G[i].f&&d[G[i].v]<0) d[G[i].v]=d[u]+1,Q[++r]=G[i].v;
return ~d[T];
}
int dfs(int u,int f) {
if(u==T) return f;
int r=f;
for(int &i=c[u],v;i;i=G[i].e) if(G[i].f&&d[v=G[i].v]==d[u]+1) {
int g=dfs(v,min(r,G[i].f));
if(!g) d[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;
}
int n,m;
vector <int> A[MAXN],B[MAXN];
void dfs1(int u,int fz) {
if(fz) *find(B[u].begin(),B[u].end(),fz)=u+m;
for(int v:B[u]) if(v<=m) dfs1(v,u);
}
vector<bool>f[MAXN];
bool in[MAXN];
void dfs3(int x) {
if(in[x]) return ; in[x]=1;
for(int i=h[x];i;i=G[i].e) if(!G[i].f) dfs3(G[i].v);
}
int op(int x) { return x>m?x-m:x+m; }
bool dfs2(int u,int fz) {
if(fz) A[u].erase(find(A[u].begin(),A[u].end(),fz));
f[u]=vector<bool>(2*m+1,0);
for(int v:A[u]) if(dfs2(v,u)) return 1;
for(int x=1;x<=m;++x) {
int cl=A[u].size(),cr=B[x].size();
if(cr==1) {
for(int v:A[u]) if(f[v][B[x][0]]) return 1;
f[u][op(B[x][0])]=1; continue;
}
if(cl<cr-1) continue;
S=cl+cr,T=S+1,init();
for(int i=0;i<cl;++i) {
link(S,i,1);
for(int j=0;j<cr;++j) if(f[A[u][i]][B[x][j]]) link(i,cl+j,1);
}
for(int i=0;i<cr;++i) link(cl+i,T,1);
int o=dinic();
if(o==cr) return 1;
if(o<cr-1) continue;
memset(in,0,T+1),dfs3(T);
for(int i=0;i<cr;++i) if(in[cl+i]) f[u][op(B[x][i])]=1;
}
return 0;
}
void solve() {
cin>>n;
for(int i=1;i<=n;++i) A[i].clear();
for(int i=1,u,v;i<n;++i) cin>>u>>v,A[u].push_back(v),A[v].push_back(u);
cin>>m;
for(int i=1;i<=m;++i) B[i].clear();
for(int i=1,u,v;i<m;++i) cin>>u>>v,B[u].push_back(v),B[v].push_back(u);
dfs1(1,0),cout<<(dfs2(1,0)?"Yes\n":"No\n");
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
X. [QOJ4243] Good Coloring (4.5)
考虑一种较优秀的调整,把每个点变成邻域颜色的 \(\mathrm{mex}\),停止时每个点邻域都有比自己颜色小 \(1\) 的点,路径容易找到,确定点权的时候从小到大确定就能让每个点不会重复调整。
时间复杂度 \(\mathcal O(n+m)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=3e5+5;
vector <int> G[MAXN],Q;
int n,m,k,a[MAXN]; bool t[MAXN];
void solve() {
cin>>n>>m>>k,Q.clear();
for(int i=1;i<=n;++i) G[i].clear();
for(int i=1;i<=n;++i) cin>>a[i],Q.push_back(i);
sort(Q.begin(),Q.end(),[&](int x,int y){ return a[x]<a[y]; });
for(int i=1,u,v;i<=m;++i) cin>>u>>v,G[u].push_back(v),G[v].push_back(u);
for(int u:Q) {
for(int v:G[u]) t[a[v]]=1;
for(a[u]=1;t[a[u]];++a[u]);
for(int v:G[u]) t[a[v]]=0;
}
int u=max_element(a+1,a+n+1)-a;
cout<<a[u]<<" "; for(int i=1;i<=n;++i) cout<<a[i]<<" \n"[i==n];
for(cout<<u<<" ";a[u]>1;cout<<u<<" ") for(int x:G[u]) if(a[x]==a[u]-1) { u=x; break; }
cout<<"\n";
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
*Y. [QOJ10003] Decorative Birds (8)
考虑判定一组解是否合法,那么每次把能删除的前缀都删除,然后判断是否有没有没删掉的解即可。
因此我们知道合法解可以看成每个时刻 \(h_t\) 在覆盖的区间中选一个 \(hh_t\),把所有 \(a_x>a_{h_t}\) 的区间都删掉。
但是直接 dp 记录 \(h\) 没办法刻画每个区间是否被重复删除。
考虑加强 \(h\) 的结构,由于 \(h\) 应该是未删除元素的最大值,我们钦定 \(h\) 如果变大成 \(h'\),则当前时刻要加入 \(a_x=h'\) 的线段,同理减小成 \(h'\) 时必须删除 \(a_x=h\) 的线段。
此时我们可以发现对于每个线段 \([l_x,r_x]\),\(a_x>h_t\) 的时间 \(t\) 构成一段区间,否则能找到一个区间 \(l_x<u<v<r_x\) 满足 \(h[u,v]\ge a_x,h_{u-1},h_{v+1}<a_x\),此时对于 \(h[u,v]\) 中的最大值 \(a_c\),其在 \(h\) 中的出现时间和消失时间都在 \([u,v+1]\) 范围内,但是由于我们 dp 时钦定的结构,则 \(u\le l_c\le r_c\le v+1\),此时 \(r_c-l_c<r_x-l_x\),与所有区间长度为 \(T\) 矛盾。
所以我们只要在每条线段首次满足条件时计算贡献,线段树维护 \(f_{i,h}\) 增大 \(h\) 只要单点修改,减小 \(h\) 则要打标记,维护 \(g_x\) 表示有多少经过 \(i\) 的线段满足 \(a>x\),则转移 \(f_{i+1,h'}\gets f_{i,h}+g_{h'}-g_h\)。
线段树维护矩阵状物,即 \(0\to f,0\to g,g\to f\) 的贡献即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5;
const ll inf=1e18;
int n,L;
struct mat { ll a,b,c; } f[1<<20|5],g[1<<20|5];
mat operator +(const mat&u,const mat&v) {
return {u.a+v.a,max(u.b+v.a,u.c+v.b),u.c+v.c};
}
void adt(int p,mat k) { f[p]=f[p]+k,g[p]=g[p]+k; }
void psd(int p) { adt(p<<1,g[p]),adt(p<<1|1,g[p]),g[p]={0,-inf,0}; }
void psu(int p) { f[p].a=max(f[p<<1].a,f[p<<1|1].a),f[p].b=max(f[p<<1].b,f[p<<1|1].b),f[p].c=max(f[p<<1].c,f[p<<1|1].c); }
void init(int l=0,int r=n,int p=1) {
f[p]={-inf,-inf,0},g[p]={0,-inf,0};
if(l==r) return ;
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void mul(int ul,int ur,mat z,int l=0,int r=n,int p=1) {
if(ul>ur) return ;
if(ul<=l&&r<=ur) return adt(p,z);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) mul(ul,ur,z,l,mid,p<<1);
if(mid<ur) mul(ul,ur,z,mid+1,r,p<<1|1);
psu(p);
}
void ins(int u,ll z,int l=0,int r=n,int p=1) {
if(l==r) return f[p].a=z,void();
int mid=(l+r)>>1; psd(p);
u<=mid?ins(u,z,l,mid,p<<1):ins(u,z,mid+1,r,p<<1|1);
psu(p);
}
ll qry(int ul,int ur,bool o,int l=0,int r=n,int p=1) {
if(ul<=l&&r<=ur) return o?max(f[p].a,f[p].b)-f[p].c:max(f[p].a,f[p].b);
int mid=(l+r)>>1; ll z=-inf; psd(p);
if(ul<=mid) z=max(z,qry(ul,ur,o,l,mid,p<<1));
if(mid<ur) z=max(z,qry(ul,ur,o,mid+1,r,p<<1|1));
return z;
}
struct seg { int t,x,w,o; } a[MAXN*2];
ll h[MAXN*2];
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n>>L;
for(int i=1,x,w,t;i<=n;++i) cin>>x>>w>>t,a[2*i-1]={t,x,w,1},a[2*i]={t+L,x,w,0};
sort(a+1,a+2*n+1,[&](auto x,auto y){ return x.t<y.t; });
init(),ins(0,0);
for(int i=1,j;i<=2*n;i=j+1) {
for(j=i;j<=2*n&&a[j+1].t==a[i].t;++j);
for(int k=i;k<=j;++k) if(!a[k].o) mul(0,a[k].x-1,{0,-inf,-a[k].w});
for(int k=i;k<=j;++k) {
if(a[k].o) h[k]=qry(0,a[k].x-1,0);
else h[k]=qry(a[k].x,a[k].x,1);
}
for(int k=i;k<=j;++k) {
if(a[k].o) ins(a[k].x,h[k]);
else mul(0,a[k].x-1,{0,h[k],0});
}
for(int k=i;k<=j;++k) if(a[k].o) mul(0,a[k].x-1,{a[k].w,-inf,a[k].w});
}
cout<<qry(0,n,0)<<"\n";
return 0;
}
Z. [QOJ8086] Cloyster (4)
考虑二分,如何比较两个相邻区域,只要分别求出它们边界上的最大值,则较大的一个所属的区域中包含全局最大值。
但这样需要 \(6n\) 次询问,考虑优化,在二分过程中我们只要找到分界线左侧最大值 \(v\),可以证明如果 \(v\) 是整个左侧边界的最大值,则右侧上的三个邻居中至少有一个 \(>v\) 才能走过去,如果 \(v\) 不是整个边界的最大值,则走进右侧的位置也一定不在分界线上。
也就是说我们只要询问分界线右侧上 \(v\) 的三个邻居即可,次数变为 \(3n+\mathcal O(\log n)\)。
时间复杂度 \(\mathcal O(n^2)\)。
代码:
#include<bits/stdc++.h>
using namespace std;
const int MAXN=2005;
mt19937 rnd(time(0));
int n,a[MAXN][MAXN],u,v;
int w(int x,int y) {
if(x<1||y<1||x>n||y>n) return 0;
if(~a[x][y]) return a[x][y];
cout<<"? "<<x<<" "<<y<<endl,cin>>a[x][y];
if(a[x][y]>a[u][v]) u=x,v=y;
return a[x][y];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
cin>>n,memset(a,-1,sizeof(a));
int lx=1,rx=n,ly=1,ry=n;
for(int m,c;lx<rx||ly<ry;) {
if(rx-lx>ry-ly) {
m=(lx+rx)>>1,c=0;
for(int i=ly;i<=ry;++i) if(w(m,i)>w(m,c)) c=i;
for(int i:{c-1,c,c+1}) w(m+1,i);
u<=m?rx=m:lx=m+1;
} else {
m=(ly+ry)>>1,c=0;
for(int i=lx;i<=rx;++i) if(w(i,m)>w(c,m)) c=i;
for(int i:{c-1,c,c+1}) w(i,m+1);
v<=m?ry=m:ly=m+1;
}
}
cout<<"! "<<lx<<" "<<ly<<endl;
return 0;
}
*AA. [QOJ4878] Easy Problem (8)
直接 Hall 定理刻画无法避免动态维护二维平面的问题。
考虑把鸡分成 \(<x,\ge x\) 的两部分,那么每部分都是一个括号匹配,只要枚举每个区间有多少个放到 \(<x\) 的位置上,多少个放到 \(\ge x\) 的位置上,然后就能用数据结构维护最小前缀和。
考虑 \(x\to x-1\),根据贪心我们应该把更多区间放到 \(\ge x\) 的位置上,那么对于序列的左侧,我们应该在最小前缀和不变的情况下进行调整,即找到最小前缀和最后一次出现的位置 \(p\),把 \(l_i\in [p+1,x]\) 中 \(r_i\) 最大的区间向后调整,要求是最小前缀和不变。
对于 \(l_i=x\) 的线段,删除时该线段已经全部放到 \(\ge x\) 部分了,可以直接删除,对于 \(r_i=x\) 的线段加入时全放到 \(<x\) 部分再调整即可,可以证明贪心策略的最优性。
考虑分析复杂度,注意到每次调整后线段树上左子树最大值大于右子树的节点会减少一个,而每次修改这样的节点只会增加 \(\mathcal O(\log n)\) 个。
时间复杂度 \(\mathcal O((n+m)\log^2n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5;
int n,m;
struct info { ll z; int x; };
bool operator<(const info&u,const info&v) { return u.z^v.z?u.z<v.z:u.x>v.x; }
struct Segt {
info tr[1<<18|5]; ll ad[1<<18|5];
void adt(int p,ll z) { tr[p].z+=z,ad[p]+=z; }
void psd(int p) { if(ad[p]) adt(p<<1,ad[p]),adt(p<<1|1,ad[p]),ad[p]=0; }
void psu(int p) { tr[p]=min(tr[p<<1],tr[p<<1|1]); }
void init(int l=0,int r=n,int p=1) {
tr[p]={0,r},ad[p]=0;
if(l==r) return ;
int mid=(l+r)>>1;
init(l,mid,p<<1),init(mid+1,r,p<<1|1);
}
void upd(int x,info z,int l=0,int r=n,int p=1) {
if(l==r) return tr[p]=z,void();
int mid=(l+r)>>1; psd(p);
x<=mid?upd(x,z,l,mid,p<<1):upd(x,z,mid+1,r,p<<1|1);
psu(p);
}
void add(int ul,int ur,ll z,int l=0,int r=n,int p=1) {
if(ul<=l&&r<=ur) return adt(p,z);
int mid=(l+r)>>1; psd(p);
if(ul<=mid) add(ul,ur,z,l,mid,p<<1);
if(mid<ur) add(ul,ur,z,mid+1,r,p<<1|1);
psu(p);
}
info qry(int ul,int ur,int l=0,int r=n,int p=1) {
if(ul<=l&&r<=ur) return tr[p];
int mid=(l+r)>>1; psd(p);
if(ur<=mid) return qry(ul,ur,l,mid,p<<1);
if(mid<ul) return qry(ul,ur,mid+1,r,p<<1|1);
return min(qry(ul,ur,l,mid,p<<1),qry(ul,ur,mid+1,r,p<<1|1));
}
} TL,TR,S;
priority_queue<array<int,2>>Q[MAXN];
vector <int> L[MAXN],R[MAXN];
int b[MAXN],c[MAXN];
ll a[MAXN],d[MAXN],e[MAXN],f[MAXN],as;
void ups(int x) { auto o=Q[x].top(); S.upd(x,info{-o[0],o[1]}); }
void solve() {
cin>>n>>m,TL.init(),TR.init(),S.init(),as=0;
for(int i=1;i<=n;++i) {
cin>>a[i],TL.add(i,n,-a[i]),as+=a[i];
priority_queue<array<int,2>>().swap(Q[i]);
L[i].clear(),R[i].clear(),Q[i].push({0,0}),ups(i);
}
for(int i=1;i<=m;++i) cin>>b[i]>>c[i]>>d[i],L[b[i]].push_back(i),R[c[i]].push_back(i),e[i]=d[i];
for(int x=n;x>=1;--x) {
for(int y:R[x]) TL.add(b[y],n,d[y]),Q[b[y]].push({c[y],y}),ups(b[y]);
while(1) {
auto h=TL.qry(0,x-1),o=S.qry(h.x+1,x);
if(!o.x) break; ll k=d[o.x];
if(b[o.x]<x) k=min(k,TL.qry(b[o.x],x-1).z-h.z);
d[o.x]-=k,TL.add(b[o.x],n,-k),TR.add(n+1-c[o.x],n,k);
if(!d[o.x]) Q[b[o.x]].pop(),ups(b[o.x]);
}
TL.add(x,n,a[x]),TR.add(n-x+1,n,-a[x]);
f[x]=as+TL.qry(0,x-1).z+TR.qry(0,n+1-x).z;
for(int y:L[x]) TR.add(n-c[y]+1,n,-e[y]);
}
for(int i=1;i<=n;++i) cout<<f[i]<<" \n"[i==n];
}
signed main() {
ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);
int _; cin>>_;
while(_--) solve();
return 0;
}
AB. [QOJ7416] Grammarly (2)
考虑什么样的路径会等价,找到第一个操作不同的位置 \(t\),则 \(t[1,|t|-1]=t[2,|t|]\),即 \(t\) 中字符全相等。
那么我们把每个序列首次得到全相等字符串的时刻取出来,然后正确计算贡献即可,此时串一定某个同色段的前缀或后缀,可以简单组合数算方案。
时间复杂度 \(\mathcal O(n)\)。
代码:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=3e5+5,MOD=998244353;
ll fc[MAXN],ic[MAXN],pw[MAXN],z;
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; }
ll C(int x,int y) { return y<0||y>x?0:fc[x]*ic[y]%MOD*ic[x-y]%MOD; }
string s; int n;
signed main() {
ios::sync_with_stdio(false),cin.tie(0);
cin>>s,n=s.size(),z=ksm(2,n)-1;
for(int i=fc[0]=pw[0]=1;i<=n;++i) fc[i]=fc[i-1]*i%MOD,pw[i]=pw[i-1]*2%MOD;
ic[n]=ksm(fc[n]); for(int i=n;i;--i) ic[i-1]=ic[i]*i%MOD;
for(int i=0,j,d;i<n;i=j) {
for(j=i+1;j<n&&s[j]==s[i];++j);
d=j-i,z=(z+C(n-d,i)*(d+MOD-pw[d]+1))%MOD;
for(int k=1;k<d;++k) z=(z+(C(n-k-1,n-j-1)+C(n-k-1,i-1))*(k+MOD-pw[k]+1))%MOD;
}
cout<<z<<"\n";
return 0;
}

浙公网安备 33010602011771号