JOISC2018 题解
\(\text{By DaiRuiChen007}\)
A. Construction of Highway
题目大意
给 \(n\) 个点,初始每个点有权值 \(w_i\),\(n-1\) 次操作连一条边 \(u\gets v\),其中 \(u\) 与 \(1\) 连通,\(v\) 与 \(1\) 不连通,求 \(1\to u\) 路径上权值的逆序对数,然后把路径权值推平为 \(v\) 的权值。
数据范围:\(n\le 10^5\)。
思路分析
考虑 LCT,注意到每次连边后 \(access(v)\),那么当前树上每条实链颜色都相同。
因此在 \(access\) 过程中对于每棵实链对应的 splay 里的点权值都一样,我们只关心 splay 里有几个点是 \(v\) 的祖先,显然把实链底 \(x\) 转到根上,那么就有 \(siz_{ls(rt)}+1\) 个颜色为 \(w_{rt}\) 的点,统计当前有多少个点 \(<w_{rt}\),然后插入树状数组对祖先贡献。
时间复杂度 \(\mathcal O(n\log^2n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+1,W=1e9;
int n,w[MAXN]; ll ans;
struct FenwickTree {
int tr[MAXN]; ll s;
vector <int> p;
inline void add(int x,int v) { for(p.push_back(x);x<=n;x+=x&-x) tr[x]+=v; }
inline ll qry(int x) { for(s=0;x;x&=x-1) s+=tr[x]; return s; }
inline void clr() { for(int x:p) for(;x<=n;x+=x&-x) tr[x]=0; p.clear(); }
} T;
namespace LCT {
int fa[MAXN],ch[MAXN][2],col[MAXN],cov[MAXN],siz[MAXN];
inline void psu(int p) { siz[p]=siz[ch[p][0]]+siz[ch[p][1]]+1; }
inline void adt(int x,int c) { if(x) cov[x]=col[x]=c; }
inline void psd(int x) { if(cov[x]) adt(ch[x][0],cov[x]),adt(ch[x][1],cov[x]),cov[x]=0; }
inline bool nrt(int p) { return ch[fa[p]][0]==p||ch[fa[p]][1]==p; }
inline int son(int x) { return ch[fa[x]][1]==x; }
inline void upd(int p) { if(nrt(p)) upd(fa[p]); psd(p); }
inline void rot(int p) {
int u=fa[p],v=fa[u],k=son(p);
if(nrt(u)) ch[v][son(u)]=p;
fa[ch[u][k]=ch[p][k^1]]=u,fa[fa[ch[p][k^1]=u]=p]=v;
psu(p),psu(u);
}
inline void splay(int p) {
for(upd(p);nrt(p);rot(p)) if(nrt(fa[p])) rot(son(p)==son(fa[p])?fa[p]:p);
}
inline int access(int p) {
int u=0;
for(ans=0,T.clr();p;u=p,p=fa[p]) {
splay(p);
int tmp=1+siz[ch[p][0]];
ans+=1ll*tmp*T.qry(col[p]-1);
T.add(col[p],tmp);
ch[p][1]=u,psu(p);
}
return u;
}
}
using namespace LCT;
signed main() {
scanf("%d",&n);
vector <int> vals;
for(int i=1;i<=n;++i) scanf("%d",&w[i]),vals.push_back(w[i]);
sort(vals.begin(),vals.end()),vals.erase(unique(vals.begin(),vals.end()),vals.end());
for(int i=1;i<=n;++i) w[i]=lower_bound(vals.begin(),vals.end(),w[i])-vals.begin()+1;
col[1]=w[1],siz[1]=1;
for(int i=1;i<n;++i) {
int u,v;
scanf("%d%d",&u,&v);
adt(access(u),w[v]);
printf("%lld\n",ans);
fa[v]=u,col[v]=w[v],siz[v]=1;
}
return 0;
}
C. Fences
题目大意
\(n\times m\) 的网格上选若干个(至少一个)格子染色并指定上下左右,满足任意两个同行或同列的格子的朝向必须相对,求方案数。
数据范围:\(n,m\le 3000\)。
思路分析
设 \(dp_{i,j}\) 表示 \(i\times j\) 网格上的答案,考虑第 \(i\) 行的情况:
- 第 \(i\) 行无黑格:从 \(dp_{i-1,j}\) 转移。
- 第 \(i\) 行有一个黑格,设在第 \(x\) 列:
- 若第 \(x\) 列只有一个黑格:那么第 \(i\) 行的黑格有 \(j\) 种位置和 \(4\) 种朝向,从 \(4\times j\times dp_{i-1,j-1}\) 转移。
- 若第 \(x\) 列有两个黑格:显然两个黑格相对,那么选定两个黑格的位置后不能选朝向,且同行不能选其他黑格,从 \((i-1)\times j\times dp_{i-2,j}\) 转移。
- 若第 \(i\) 行有两个黑格:显然对应两列不能放其他黑格,从 \(\dfrac{j(j-1)}2\times dp_{i-1,j-2}\) 转移。
因此得到状态转移方程:
时间复杂度 \(\mathcal O(nm)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=3001,MOD=1e9+7;
int dp[MAXN][MAXN];
signed main() {
int n,m;
scanf("%lld%lld",&n,&m);
for(int i=0;i<=max(n,m);++i) dp[0][i]=dp[i][0]=1;
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) {
dp[i][j]=(dp[i-1][j]+4*j*dp[i-1][j-1])%MOD;
if(i>1) dp[i][j]=(dp[i][j]+(i-1)*j%MOD*dp[i-2][j-1]%MOD)%MOD;
if(j>1) dp[i][j]=(dp[i][j]+j*(j-1)/2%MOD*dp[i-1][j-2]%MOD)%MOD;
}
printf("%lld\n",(dp[n][m]+MOD-1)%MOD);
return 0;
}
D. Asceticism
题目大意
给定 \(n,m\),求有多少长度为 \(n\) 的排列 \(p_i\) 满足 \(\sum_{i=1}^{n-1}[p_i>p_{i+1}]=m-1\)。
数据范围:\(n,m\le 10^5\)。
思路分析
对于这类求升高的问题,经典的想法就是容斥,钦定 \(k\) 个位置必须是 \(p_i>p_{i+1}\),求答案。
此时把连续的 \(>\) 看成一段,那么整个序列被分成了 \(n-k\) 段,段内定序,大小记为 \(s_1\sim s_{n-k}\),那么方案数就是 \(\dfrac{n!}{\prod_{i=1}^{n-k}s_i!}\)。
注意到这实际上等价于将 \(n\) 个不同的球放进 \(n-k\) 个不同的非空盒子里的方案数,容易证明两个组合问题之间存在双射,容斥可得此时的方案数为:
二项式反演得到:
其中最后一步我们用到了如下恒等式:
组合意义解释为:\(n+1\) 个数里选 \(a+b+1\) 个,枚举第 \(a+1\) 个数的位置,前 \(i\) 个数里选 \(a\) 个,后 \(n-i\) 个数里选 \(b\) 个。
注意我们要求的答案是 \(\left\langle\begin{matrix}n\\m-1\end{matrix}\right\rangle\),直接暴力计算。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+5,MOD=1e9+7;
ll fac[MAXN],ifac[MAXN];
inline ll ksm(ll a,ll b=MOD-2,ll p=MOD) {
ll ret=1;
for(;b;a=a*a%p,b=b>>1) if(b&1) ret=ret*a%p;
return ret;
}
inline ll binom(int n,int m) { return fac[n]*ifac[m]%MOD*ifac[n-m]%MOD; }
signed main() {
int n,m;
scanf("%d%d",&n,&m),--m;
for(int i=fac[0]=ifac[0]=1;i<=n+1;++i) ifac[i]=ksm(fac[i]=fac[i-1]*i%MOD);
ll ans=0;
for(int i=1;i<=n-m;++i) ans=(ans+MOD+((n-m-i)%2?-1:1)*ksm(i,n)*binom(n+1,m+i+1)%MOD)%MOD;
printf("%lld\n",ans);
return 0;
}
E. Road Service
题目大意
本题为提交答案题。
给定一棵 \(n\) 个点的树,在上面增加 \(k\) 条无向边,最小化 \(\sum_{1\le i<j\le n} \mathrm{dist}(i,j)\),其中 $ \mathrm{dist}(i,j)$ 表示 \(i\to j\) 的最短路长度。
数据范围:\(n\le 1000\)。
思路分析
容易发现比较优的解一定是把 \(k\) 条边连成一个菊花。
考虑模拟退火,每次更改一条边的端点,但 \(\mathcal O(n^2)\) 的估价函数会使得模拟退火比较难算,考虑用 \(\mathcal O(n)\) 的估价函数替代,不妨取 \(\sum_{1\le i\le n}\mathrm{dist}(rt,i)\) 当估价函数,\(rt\) 表示附加边的中心点。
对于初始态,我们可以对枚举起点 \(rt\),随机一种方案,算出实际权值然后排序,取最小的 \(10\) 组方案当初始态退火即可。
但这样退火的初始态也不够优秀,考虑按度数从大到小将点添加进 \(rt\) 的邻域,稍微随机扰动一下即可。
挂一会就跑出来了。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1005,inf=1e9;
mt19937 rnd(time(0));
int n,k,w0;
inline double rd() {
return uniform_real_distribution<>(0,1)(rnd);
}
bool vis[MAXN];
int d[MAXN],bs,ans=inf,que[MAXN];
vector <int> G[MAXN],E,R,S[MAXN][5];
inline int qval(int ht) {
memset(d,0x3f,sizeof(d));
int hd=1,tl=1;
que[1]=ht,d[ht]=0;
for(int i:E) que[++tl]=i,d[i]=1;
while(hd<=tl) {
int u=que[hd++];
for(int v:G[u]) if(d[v]>d[u]+1) {
d[v]=d[u]+1,que[++tl]=v;
}
}
return accumulate(d+1,d+n+1,0);
}
inline int qvalALL(int ht,int ti) {
int sum=0;
for(int i=1;i<=n;++i) {
memset(d,0x3f,sizeof(d));
int hd=1,tl=1;
que[1]=i,d[i]=0;
while(hd<=tl) {
int u=que[hd++];
for(int v:G[u]) if(d[v]>d[u]+1) {
d[v]=d[u]+1,que[++tl]=v;
}
if(u==ht) {
for(int v:S[ht][ti]) if(d[v]>d[u]+1) {
d[v]=d[u]+1,que[++tl]=v;
}
}
if(vis[u]) {
if(d[ht]>d[u]+1) {
d[ht]=d[u]+1,que[++tl]=ht;
}
}
}
sum+=accumulate(d+1,d+n+1,0);
}
return sum;
}
int rk[MAXN];
double wt[MAXN];
signed main() {
scanf("%d%d%d",&n,&k,&w0);
for(int i=0,u,v;i<n;++i) scanf("%d%d",&u,&v),G[u].push_back(v),G[v].push_back(u);
vector <array<int,3>> vals;
for(int ht=1;ht<=n;++ht) for(int ti:{0,1,2,3,4}) {
memset(vis,false,sizeof(vis));
for(int i=1;i<=n;++i) if(i!=ht) wt[i]=G[i].size()+rd()*10;
wt[ht]=-inf;
for(int i=1;i<=n;++i) rk[i]=i;
sort(rk+1,rk+n+1,[&](int x,int y){ return wt[x]<wt[y]; });
for(int i=1;i<=k;++i) S[ht][ti].push_back(rk[i]),vis[rk[i]]=true;
vals.push_back({qvalALL(ht,ti),ht,ti});
}
sort(vals.begin(),vals.end());
for(int t=0;t<10;++t) {
int ht=vals[t][1],ti=vals[t][2];
memset(vis,false,sizeof(vis));
E=S[ht][ti];
int tmp=qval(ht);
auto get=[&]() {
int x;
do x=rnd()%n+1;
while(x==ht||vis[x]);
return x;
};
if(tmp<ans) ans=tmp,R=E,bs=ht;
for(double T=10000;T>1e-10;T*=0.9999) {
int i=get(),id=rnd()%k,j=E[id];
vis[j]=false,vis[i]=true,E[id]=i;
tmp=qval(ht);
if(tmp<ans) ans=tmp,bs=ht,R=E;
else if(rnd()<=exp((ans-tmp)/T));
else vis[j]=true,vis[i]=false,E[id]=j;
}
}
for(int i:R) printf("%d %d\n",bs,i);
}
F. Worst Reporter 3
题目大意
数轴上有 \(n+1\) 个人,编号 \(0\sim n\),第 \(i\) 个人初始在 \(-i\) 上。
每轮第 \(0\) 个人向右移动 \(1\),然后对于 \(i=1\sim n\),如果其与 \(i-1\) 距离 \(<d_i\),那么就移动到第 \(i-1\) 左边的位置。
\(q\) 次询问 \(t\) 秒后 \([l,r]\) 内有几个人。
数据范围:\(n,q\le 5\times 10^5\)。
思路分析
根据数学归纳法可以证明每个人的运动都是具有周期性的,即每隔 \(k_i\) 轮动 \(k_i\) 步。
然后二分 \(t\) 秒后位置 \(\le r\) 和 \(\le l-1\) 的有几个人即可求出答案。
事实上,注意到 \(k_{i-1}\mid k_i\),因此本质不同的 \(k_i\) 只有 \(\mathcal O(\log V)\) 级别,暴力找对应端点也可以通过。
时间复杂度 \(\mathcal O(n+q\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=5e5+1;
int d[MAXN],k[MAXN];
signed main() {
int n,q;
scanf("%lld%lld",&n,&q);
for(int i=1;i<=n;++i) scanf("%lld",&d[i]);
d[0]=k[0]=1;
for(int i=1;i<=n;++i) k[i]=(d[i]+k[i-1]-1)/k[i-1]*k[i-1];
auto query=[&](int t,int x) {
int l=0,r=n,res=-1;
while(l<=r) {
int mid=(l+r)>>1;
if((t/k[mid])*k[mid]-mid>=x) res=mid,l=mid+1;
else r=mid-1;
}
return res+1;
};
while(q--) {
int t,l,r;
scanf("%lld%lld%lld",&t,&l,&r);
printf("%lld\n",query(t,l)-query(t,r+1));
}
return 0;
}
G. Airline Route Map
题目大意
通信题。
A.cpp
输入一张 \(n\) 个点的图 \(G\),输出一个点数不超过 \(n+12\) 的图 \(H\)。- 交互器任意打乱 \(H\) 中点和边的标号得到 \(H'\)。
B.cpp
输入 \(H'\),还原 \(G\)。数据范围:\(n\le 1000\)。
思路分析
思路肯定是要用多的 \(12\) 个点表示原图的 \(n\) 个点原来的标号,因此很自然地想到二进制分组。
用其中的 \(10\) 个点 \(v_0\sim v_9\) 表示,\(v_i\) 分别连接 \(1\sim n\) 中第 \(i\) 个二进制位为 \(1\) 的所有点。
那么原问题就变成了如何找到 \(v_0\sim v_9\),考虑从 B.cpp
的角度思考,观察我们能快速在 \(H'\) 中提取什么信息。
在所有点都不确定的情况下,最容易想到的就是有关度数的信息,可以考虑让 \(v_{11}\) 连除了 \(v_{10}\) 之外的所有点。
那么找度数最大的点 \(v_{11}\),再找 \(v_{11}\) 邻域之外的 \(v_{10}\),就能确定出某一个特定的点了。
考虑用 \(v_{10}\) 传递什么信息:显然是如何区分 \(v_0\sim v_9\) 和其他点,那么就连 \(v_{10}\to v_0\sim v_9\),问题只剩下如何确定这些点之间的顺序。
谈到确定顺序,显然的想法是把 \(v_0\to v_1\to v_2\to\cdots\to v_9\) 连成链。由于链是无向的,最后我们要区分 \(v_0/v_9\) 即可,显然任何情况下 \(\mathrm{deg}(v_0)>\mathrm{deg}(v_9)\),那么这题就做完了。
代码呈现
A.cpp
:
#include <bits/stdc++.h>
#include "Alicelib.h"
using namespace std;
void Alice(int N,int M,int A[],int B[]){
vector <array<int,2>> Edges;
for(int i=0;i<M;++i) Edges.push_back({A[i],B[i]});
for(int i=0;i<N;++i) {
for(int k=0;k<10;++k) {
if((i>>k)&1) Edges.push_back({i,N+k});
}
}
for(int i=0;i<10;++i) Edges.push_back({N+i,N+10});
for(int i=1;i<10;++i) Edges.push_back({N+i-1,N+i});
for(int i=0;i<N+10;++i) Edges.push_back({N+11,i});
InitG(N+12,Edges.size());
for(int i=0;i<(int)Edges.size();++i) MakeG(i,Edges[i][0],Edges[i][1]);
}
B.cpp
:
#include<bits/stdc++.h>
#include "Boblib.h"
using namespace std;
void Bob(int V,int U,int C[],int D[]){
vector <int> degs(V);
vector <vector<int>> adj(V);
for(int i=0;i<U;++i) {
adj[C[i]].push_back(D[i]);
adj[D[i]].push_back(C[i]);
++degs[C[i]],++degs[D[i]];
}
int st=0;
for(int i=0;i<V;++i) if(degs[i]>degs[st]) st=i;
assert(degs[st]==V-2);
vector <int> typ(V,2);//graph_node[0] bit_seq[1] bit_marker[2] start_node[3]
typ[st]=3;
for(int u:adj[st]) typ[u]=0;
int bm=-1; //bit_marker
for(int i=0;i<V;++i) if(typ[i]==2) { bm=i; break; }
assert(bm!=-1);
for(int u:adj[bm]) typ[u]=1;
vector <array<int,2>> b_adj(V,{V,V}); //adj in bit_seq
int b_top=-1; //top of the bit_seq
for(int u:adj[bm]) {
for(int v:adj[u]) {
if(typ[v]==1) {
if(b_adj[u][0]<V) b_adj[u][1]=v;
else b_adj[u][0]=v;
}
}
if(b_adj[u][1]==V) b_top=u;
}
assert(b_top!=-1);
vector <int> bs{b_top}; //bit_seq
for(int p=b_top,fa=V;;) {
int tmp=p;
p=b_adj[p][0]^b_adj[p][1]^fa,fa=tmp;
bs.push_back(p);
if(b_adj[p][1]==V) break;
}
assert(bs.size()==10);
if(degs[bs.front()]<degs[bs.back()]) reverse(bs.begin(),bs.end());
vector <int> idx(V);
for(int k=0;k<10;++k) {
for(int u:adj[bs[k]]) if(!typ[u]) idx[u]|=1<<k;
}
vector <array<int,2>> Edges;
for(int i=0;i<U;++i) if(!typ[C[i]]&&!typ[D[i]]) Edges.push_back({idx[C[i]],idx[D[i]]});
InitMap(V-12,Edges.size());
for(auto e:Edges) MakeMap(e[0],e[1]);
}
H. Bitaro's Party
题目大意
给定一张 \(n\) 个点 \(m\) 条边的有向图,\(q\) 次询问 \(x_i\) 并给定 \(k_i\) 个点,求 \(1\sim n\) 中除这 \(x_i\) 个点之外的点到 \(x_i\) 的最长距离。
数据范围:\(n,q,k=\sum k_i\le 10^5,m\le 2\times 10^5\),每条边都是从编号小的点连向到编号大的点。
思路分析
考虑根号分治,若 \(k_i\le \sqrt n\),我们只关心最短的 \(\sqrt n\) 条路径,拓扑排序的过程中每次归并排序合并答案,注意同一个点不要被多次统计。
\(k_i>\sqrt n\) 的情况暴力处理,这样的询问只有 \(\mathcal O\left(\dfrac {k}{\sqrt n}\right)\) 个,\(\mathcal O(n+m)\) 暴力求即可。
时间复杂度 \(\mathcal O((n+m+k)\sqrt n)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=1e5+5,inf=2e9;
vector <array<int,2>> P[MAXN];
vector <int> G[MAXN];
int dis[MAXN],mark[MAXN],mp[MAXN];
signed main() {
int n,m,q;
scanf("%d%d%d",&n,&m,&q);
int B=sqrt(n);
for(int i=1;i<=m;++i) {
int u,v;
scanf("%d%d",&u,&v);
G[v].push_back(u);
}
fill(mp+1,mp+n+1,-inf);
for(int u=1;u<=n;++u) {
P[u].push_back({0,u});
for(int v:G[u]) for(auto I:P[v]) mp[I[1]]=max(mp[I[1]],I[0]+1);
for(int v:G[u]) {
int ed=P[u].size();
for(auto I:P[v]) if(mp[I[1]]==I[0]+1) {
P[u].push_back({mp[I[1]],I[1]}),mp[I[1]]=-inf;
}
inplace_merge(P[u].begin(),P[u].begin()+ed,P[u].end(),greater<array<int,2>>());
if((int)P[u].size()>B) P[u].resize(B);
}
}
for(int T=1;T<=q;++T) {
int t,y;
scanf("%d%d",&t,&y);
for(int i=0,j;i<y;++i) scanf("%d",&j),mark[j]=T;
for(auto p:P[t]) {
if(mark[p[1]]<T) {
printf("%d\n",p[0]);
goto ok;
}
}
if((int)P[t].size()<B) puts("-1");
else {
fill(dis+1,dis+t+1,-inf),dis[t]=0;
for(int u=t;u;--u) {
for(int v:G[u]) dis[v]=max(dis[v],dis[u]+1);
}
int ans=-1;
for(int i=1;i<=t;++i) if(mark[i]<T) ans=max(ans,dis[i]);
printf("%d\n",ans<0?-1:ans);
}
ok:;
}
return 0;
}
I. Security Gate
题目大意
定义一个括号序列是“好的”当且仅当能够将一个该括号序列的一个可空子串的左右括号反转从而得到一个合法括号序列。
给定一个长度为 \(n\) 有空白位置的括号串,求有多少种在空白中填左括号或右括号的方式,使得最终的括号串是“好的”。
数据范围:\(n\le 300\)。
思路分析
令左括号为 \(1\),右括号为 \(-1\),设翻转区间为 \((l,r]\)。
定义括号序列翻转表示整个序列变成 \(s_ns_{n-1}\dots s_1\) 然后反转左右括号。
设第 \(i\) 个点对应前缀和为 \(S_i\),那么一个括号序列序列合法当且仅当翻转前后所有前缀和均非负, 即 \(S_i\ge 0,S_i-S_n\ge 0\)。
分类讨论两侧的合法情况。
-
两侧前缀和均合法。
那么显然整个括号序列合法,直接简单 dp 即可。
-
恰有一侧前缀和不合法。
不妨假设所有 \(S_i\ge S_n\) 但存在 \(S_i<0\)。
找到第一个 \(S_i=-1\) 的位置 \(p\),一定有 \(l\le p\)。
注意到从 \(S_l\) 越大的位置翻转越优,因此直接找 \(S_1\sim S_p\) 中的最大值作为 \(S_l\)。
注意到 \(i>r\) 的点翻转后的前缀和会变成 \(S_i-S_n\),这些位置一定合法。
设 \(a=S_l,b=S_n\),那么我们要找一个 \(S_r=a+b/2\),显然 \(r\) 越小越好。
如果 \(a+b/2\ge 0\),那么可以在 \((l,p]\) 中找到一个合法的 \(r\)。
否则我们要 dp,满足 \(S_p\sim S_r\) 中不能有超过 \(2a\) 的数。
考虑从后往前维护 \(S_i-S_n\) 的值 \(T_i\),我们要找的 \(r\) 就是 \(T_r=a-b/2\),限制就是 \(\forall i\in [p,r]\) 满足 \(T_i\le 2a-b= 2(a-b/2)\)。
枚举 \(a-b/2\) 然后 dp,前缀只要记录当前和和历史最大前缀和简单预处理,设 \(f_{i,a}\) 表示 \(S_{i}=-1\) 且 \(\max\{S_1\sim S_i\}=a\) 的方案数。
后缀 dp 设 \(dp_{i,j,k}\) 表示 \(T_i=j\),\(k=0\) 表示没确定 \(r\),\(k=1\) 表示确定了 \(r\)。
最终答案为 \(\sum_i\sum _a f_{i,a}\times dp_{i,-b-1,[a+b/2]<0}\)。
-
两侧前缀均不合法。
找到第一个 \(S_i=-1\) 的位置 \(p\) 和最后一个 \(S_i-S_n=-1\) 的位置 \(q\)。
设 \(S_1\sim S_i\) 的最大值为 \(a\),\(S_n=b\),那么我们要在 \(S_q\sim S_n\) 范围内找一个 \(a+b/2\)。
不妨设 \(\max\{S_q\sim S_n\}=c\),那么要求 \(a+b/2\le c\),若 \(a+b/2>c\),则将整个序列翻转后得到 \(a'+b'/2< c'\)。
依然是将整个序列减掉 \(S_n\),设 \(T_i=S_i-S_n\),我们依然要找 \(T_r=a-b/2\)。
前缀 dp 依然不变,后缀 dp 设 \(dp_{i,j,k}\) 表示 \(T_i=j\),\(k=0\) 表示没确定 \(r\),\(k=1\) 表示确定了 \(r\),\(k=2\) 表示确定了 \(q\)。
若 \(k\ne 0\),那么要求 \(T_i\le 2a-b=2(a-b/2)\),和上一部分一样简单 dp,注意 \(j\) 在 \(k=2\) 时可以 \(<0\)。
最终答案为 \(\sum_i\sum_af_{i,a}\times dp_{i,-b-1,2}\)。
但这样会算重贡献,即 \(a+b/2=c\) 的情况会被算两次,那么单独求一下这种情况的方案数。
上面的 dp 一样,但在 \(k\ne 2\) 时需要满足 \(T_i\le a-b/2\),其他不变。
分别用 dp 处理即可。
时间复杂度 \(\mathcal O(n^3)\)。
代码呈现
#include<bits/stdc++.h>
using namespace std;
const int MAXN=305,MOD=1e9+7;
int n;
char str[MAXN];
inline void add(int &x,int y) { x=(x+y>=MOD)?x+y-MOD:x+y; }
inline int DP0() { //both side valid
static int dp[MAXN][MAXN];
//dp[i,j]: S[i]=j
memset(dp,0,sizeof(dp));
dp[0][0]=1;
for(int i=0;i<n;++i) for(int j=0;j<=i;++j) if(dp[i][j]) {
if(str[i+1]!=')') add(dp[i+1][j+1],dp[i][j]);
if(str[i+1]!='('&&j>0) add(dp[i+1][j-1],dp[i][j]);
}
return dp[n][0];
}
int f[MAXN][MAXN];
//f[i,j]: S[1~i]=-1, max prefix=j
inline void DP1() {
static int dp[2][MAXN][MAXN];
//dp[i,j,k]: S[i]=j, max prefix=k
memset(f,0,sizeof(f));
memset(dp,0,sizeof(dp));
dp[0][0][0]=1;
for(int i=0;i<n;++i) {
int cur=i&1; memset(dp[cur^1],0,sizeof(dp[cur^1]));
for(int j=0;j<=i;++j) for(int k=j;k<=i;++k) if(dp[cur][j][k]) {
if(str[i+1]!=')') add(dp[cur^1][j+1][max(k,j+1)],dp[cur][j][k]);
if(str[i+1]!='(') {
if(j>0) add(dp[cur^1][j-1][k],dp[cur][j][k]);
else add(f[i+1][k],dp[cur][j][k]);
}
}
}
}
inline int DP2() { //prefix invalid, suffix valid
static int dp[MAXN][MAXN][2];
//dp[i,j,k]: revS[i]=j, k={0: not fix r,1: fix r}
int ans=0;
for(int s=0;s<=n;++s) { //s=a-b/2
memset(dp,0,sizeof(dp));
dp[n][0][0]=1;
for(int i=n;i>=1;--i) for(int j=0;j<=n-i;++j) for(int k:{0,1}) {
if(k&&j>s*2) dp[i][j][k]=0; //exceed 2a
if(k&&j==s) dp[i][j][k]=dp[i][j][0]; //find a+b/2
if(dp[i][j][k]) {
if(str[i]!='(') add(dp[i-1][j+1][k],dp[i][j][k]);
if(str[i]!=')'&&j>0) add(dp[i-1][j-1][k],dp[i][j][k]);
}
}
for(int i=1;i<=n;++i) { //pos of first -1
for(int a=0;a<=n;++a) if(f[i][a]) {
int b=(a-s)*2; //S[1~n]
if(-n<=b&&b<0) {
add(ans,1ll*f[i][a]*dp[i][-b-1][a+b/2<0]%MOD);
//if a+b/2>=0, we can get a+b/2 in S[1]~S[i]
}
}
}
}
return ans;
}
inline int DP3(bool flg) { //both side invalid
//flg={0:c >= a+b/2,1:c = a+b/2}
static int dp[MAXN][MAXN<<1][3];
//dp[i,j,k]: revS[i]=j-n, k={0: not fix r, 1: fix r, 2: fix last -1}
int ans=0;
for(int s=0;s<=n;++s) { //s=a-b/2
memset(dp,0,sizeof(dp));
dp[n][n][0]=1;
for(int i=n;i>=1;--i) for(int j=i-n;j<=n-i;++j) for(int k:{0,1,2}) {
if(k!=0&&j>s*2) dp[i][j+n][k]=0; //exceed 2a
if(k==1&&j==s) dp[i][j+n][k]=dp[i][j+n][0]; //find a+b/2
if(flg&&k!=2&&j>s) dp[i][j+n][k]=0; //flg: c can not exceed a+b/2
if(dp[i][j+n][k]) {
if(str[i]!='(') add(dp[i-1][j+1+n][k],dp[i][j+n][k]);
if(str[i]!=')') {
if(j>0) add(dp[i-1][j-1+n][k],dp[i][j+n][k]);
else if(k!=0) add(dp[i-1][j-1+n][2],dp[i][j+n][k]);
//-1 can only appear in part of k=2
}
}
}
for(int i=1;i<=n;++i) { //pos of first -1
for(int a=0;a<=n;++a) if(f[i][a]) {
int b=(a-s)*2;
if(-n<=b&&b<=n) add(ans,1ll*f[i][a]*dp[i][-b-1+n][2]%MOD);
}
}
}
return ans;
}
signed main() {
scanf("%d%s",&n,str+1);
if(n&1) return puts("0"),0;
DP1();
int ans=(0ll+DP0()+DP2()+DP3(0)+MOD-DP3(1))%MOD;
reverse(str+1,str+n+1);
for(int i=1;i<=n;++i) if(str[i]!='x') str[i]^=1;
DP1();
ans=(0ll+ans+DP2()+DP3(0))%MOD;
printf("%d\n",ans);
return 0;
}
J. Candies
题目大意
给一个长度为 \(n\) 的数轴,每个位置有权值,对于 \(k=1\sim \left\lceil\dfrac n2\right\rceil\),求出选 \(k\) 个不相邻位置的最大权值和。
数据范围:\(n\le 2\times 10^5\)。
思路分析
观察解的形态,用 \(0/1\) 表示该位置有没有被选,容易发现序列能划分成形如 \(0,1\) 交替出现的段且两端为 \(0\),每次操作一定是把某个段 \(01\) 取反,然后把两边的段合并。
用链表维护合并的过程,堆贪心求最大的一段操作即可。
时间复杂度 \(\mathcal O(n\log n)\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2e5+5,INF=1e18;
int pre[MAXN],suf[MAXN],inq[MAXN],c[MAXN];
signed main() {
int n;
scanf("%lld",&n);
for(int i=1;i<=n;++i) scanf("%lld",&c[i]),pre[i]=i-1,suf[i]=i+1,inq[i]=1;
c[0]=c[n+1]=-INF;
auto merge=[&](int u) {
pre[suf[u]]=pre[u];
suf[pre[u]]=suf[u];
inq[u]=0;
};
priority_queue <array<int,2>> Q;
for(int i=1;i<=n;++i) Q.push({c[i],i});
int ans=0;
for(int i=1;i<=(n+1)/2;++i) {
while(!inq[Q.top()[1]]) Q.pop();
int w=Q.top()[0],p=Q.top()[1]; Q.pop();
printf("%lld\n",ans+=w);
Q.push({c[p]=c[pre[p]]+c[suf[p]]-w,p});
merge(pre[p]),merge(suf[p]);
}
return 0;
}
K. Library
题目大意
交互器有一个 \(1\sim n\) 的排列 \(p\),你每次可以询问 \(S\) 里的元素在 \(p\) 上构成了几个连续段。
在 \(20000\) 次询问内还原出排列。
数据范围:\(n\le 1000\)。
思路分析
考虑增量法,注意到询问 \(S\) 和 \(S\cup \{x\}\) 后连续段数的变化情况。
先用 \(2n\) 次询问求出排列的一个端点。
然后动态维护已知前缀 \(P\),对于未确定集合 \(Q\),二分成两个集合 \((X,Q\text{\\}X)\),询问 \(X\) 和 \(P\cup X\) 即可知道 \(X\) 里有没有前缀的下一个元素,不断操作即可。
交互次数 \(2(n+n\log n)\),但事实上后面的 \(\log |Q|\) 大部分情况下取不到 \(10\),因此可以通过。
时间复杂度 \(\mathcal O(n^2\log n)\)。
代码呈现
#include<bits/stdc++.h>
#include "library.h"
using namespace std;
void Solve(int N) {
if(N==1) { Answer({1}); return ; }
int st=-1;
vector <int> rest;
auto Adj=[&](int x,vector <int> V) -> int {
vector <int> q(N,0);
for(int v:V) q[v]=1;
int d=Query(q);
q[x]=1;
return d-Query(q)+1;
};
for(int i=0;i<N;++i) {
vector <int> V;
for(int j=0;j<N;++j) if(i!=j) V.push_back(j);
int t=Adj(i,V);
if(t==1) { st=i,rest=V; break; }
}
vector <int> ans{st};
while((int)ans.size()<N) {
auto Ne=[&](int x,vector <int> V) -> int {
while(V.size()>1) {
int mid=V.size()>>1;
vector <int> V0(V.begin(),V.begin()+mid),V1(V.begin()+mid,V.end());
V=Adj(x,V0)?V0:V1;
}
return V[0];
};
int u=ans.back(),v=Ne(u,rest);
rest.erase(lower_bound(rest.begin(),rest.end(),v));
ans.push_back(v);
}
for(int &u:ans) ++u;
Answer(ans);
}
L. Wild Boar
题目大意
给一张 \(n\) 个点 \(m\) 条边的无向图,定义一条路径合法当且仅当不会连续经过同一条边。
动态维护一个长度为 \(k\) 的序列 \(v_1\sim v_k\),支持 \(q\) 次单点修改,每次求出顺次经过 \(v_1\sim v_k\) 的最短合法路径。
数据范围:\(n,m\le 2000,q,k\le 10^5\)。
思路分析
观察到任何一段 \(v_i\to v_{i+1}\) 的路径的第一条边 \(s\) 和最后一条边 \(t\) 只有可能是如下四种情况之一:
- \((s_1,t_1)\):\(v_i\to v_{i+1}\) 的最短路。
- \((s_2,t_2)\):\(s_2\ne s_1,t_2\ne t_1\) 的最短路。
- \((s_3,t_3)\):\(s_3\ne s_1,t_3\ne t_2\) 的最短路。
- \((s_4,t_4)\):\(s_4\ne s_2,t_4\ne t_1\) 的最短路。
暴力 Dijkstra 求出边之间的全源最短路,然后对每对点预处理出这四条路径。
那么我们 dp 就只用记录 \(v_{i-1}\to v_i\) 走的是哪条路径,修改可以用动态 dp 维护矩阵乘法解决。
时间复杂度 \(\mathcal O(m^2\log m+\delta^3(k+q)\log k)\),其中 \(\delta=4\)。
代码呈现
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int MAXN=2005,MAXM=4005,MAXL=1e5+1,INF=1e18;
int n,m,q,len,ecnt=0,head[MAXN];
struct Edge {
int v,w,lst;
} G[MAXM];
inline void AddEdge(int u,int v,int w) { G[ecnt]={v,w,head[u]},head[u]=ecnt++; }
struct Node {
int s,t,c;
};
int dist[MAXM][MAXM];
bool vis[MAXM];
inline Node GetPath(int u,int v,int lst,int nxt) {
Node P={-1,-1,INF};
for(int s=head[u];s!=-1;s=G[s].lst) {
for(int t=head[v];t!=-1;t=G[t].lst) {
if((lst==-1||s!=lst)&&(nxt==-1||(t^1)!=nxt)&&dist[s][t^1]<P.c) {
P={s,t^1,dist[s][t^1]};
}
}
}
return P;
}
Node Info[MAXN][MAXN][4];
/* u[0],v[0]: shortest path
* u[1],v[1]: u[1]!=u[0],v[1]!=v[0]
* u[2],v[2]: u[2]!=u[0],v[2]!=v[1]
* u[3],v[3]: u[3]!=u[1],v[3]!=v[0]
*/
struct Matrix {
int f[4][4];
Matrix() { memset(f,0x3f,sizeof(f)); }
inline int* operator [](int i) { return f[i]; }
inline friend Matrix operator *(Matrix u,Matrix v) {
Matrix w;
for(int k:{0,1,2,3}) for(int i:{0,1,2,3}) for(int j:{0,1,2,3}) {
w[i][j]=min(w[i][j],u[i][k]+v[k][j]);
}
return w;
}
};
int ord[MAXL];
inline Matrix GetMatrix(int idx) {
Matrix m;
int a=ord[idx-1],b=ord[idx],c=ord[idx+1];
for(int u:{0,1,2,3}) for(int v:{0,1,2,3}) {
if(~Info[a][b][u].t&&~Info[b][c][v].s&&(Info[a][b][u].t^Info[b][c][v].s)!=1) {
m[u][v]=Info[b][c][v].c;
}
}
return m;
}
Matrix sum[MAXL<<2];
inline void Build(int l=2,int r=len-1,int pos=1) {
if(l==r) { sum[pos]=GetMatrix(l); return ; }
int mid=(l+r)>>1;
Build(l,mid,pos<<1),Build(mid+1,r,pos<<1|1);
sum[pos]=sum[pos<<1]*sum[pos<<1|1];
}
inline void Modify(int u,int l=2,int r=len-1,int pos=1) {
if(u<l||u>r) return ;
if(l==r) { sum[pos]=GetMatrix(l); return ; }
int mid=(l+r)>>1;
if(u<=mid) Modify(u,l,mid,pos<<1);
else Modify(u,mid+1,r,pos<<1|1);
sum[pos]=sum[pos<<1]*sum[pos<<1|1];
}
signed main() {
scanf("%lld%lld%lld%lld",&n,&m,&q,&len);
memset(head,-1,sizeof(head));
for(int i=1;i<=m;++i) {
int u,v,w;
scanf("%lld%lld%lld",&u,&v,&w);
AddEdge(u,v,w),AddEdge(v,u,w);
}
memset(dist,0x3f,sizeof(dist));
for(int st=0;st<ecnt;++st) {
priority_queue <array<int,2>,vector<array<int,2>>,greater<array<int,2>>> Q;
Q.push({dist[st][st]=G[st].w,st});
memset(vis,false,sizeof(vis));
while(!Q.empty()) {
int e=Q.top()[1]; Q.pop();
if(vis[e]) continue; vis[e]=true;
for(int f=head[G[e].v];f!=-1;f=G[f].lst) {
if(dist[st][f]>dist[st][e]+G[f].w&&(e^f)!=1) {
Q.push({dist[st][f]=dist[st][e]+G[f].w,f});
}
}
}
}
for(int u=1;u<=n;++u) for(int v=1;v<=n;++v) {
Info[u][v][0]=GetPath(u,v,-1,-1);
Info[u][v][1]=GetPath(u,v,Info[u][v][0].s,Info[u][v][0].t);
Info[u][v][2]=GetPath(u,v,Info[u][v][0].s,Info[u][v][1].t);
Info[u][v][3]=GetPath(u,v,Info[u][v][1].s,Info[u][v][0].t);
}
for(int i=1;i<=len;++i) scanf("%lld",&ord[i]);
if(len==2) {
while(q--) {
int x,y;
scanf("%lld%lld",&x,&y);
ord[x]=y;
printf("%lld\n",Info[ord[1]][ord[2]][0].c);
}
} else {
Build();
while(q--) {
int x,y;
scanf("%lld%lld",&x,&y);
ord[x]=y;
Modify(x-1),Modify(x),Modify(x+1);
int ans=INF;
for(int i:{0,1,2,3}) for(int j:{0,1,2,3}) {
ans=min(ans,Info[ord[1]][ord[2]][i].c+sum[1][i][j]);
}
printf("%lld\n",ans<INF?ans:-1);
}
}
return 0;
}