九省联考
一双木棋
Luogu
LOJ
BZOJ
为了方便我们把整个过程倒过来,保证最后一步行动的人是先手(菲菲)。
不难发现任意时刻都存在一个右上-左下的轮廓线。
这个轮廓线从((0,m)
ightarrow(n,0)),共有(n+m)条边,我们用(0)表示向左,(1)表示向下,那么一条轮廓线就可以按顺序被压缩成一个(n+m)位的二进制数。
然后我们用(0,1)分别表示先手后手,设(f_{i,j})表示轮廓线状态为(i),下一个操作的人是(j)时(!j)的最高得分。
转移就找到轮廓线中所有“下-右”型的可以放棋子的角落,然后枚举放哪个即可。
#include<cstdio>
#include<cstring>
int a[2][12][12],f[1<<20][2];
int read(){int x;scanf("%d",&x);return x;}
void max(int&a,int b){if(b>a)a=b;}
int main()
{
int n=read(),m=read();memset(f,168,1<<23),f[(1<<n)-1][!((n*m)&1)]=0;
for(int i=0;i<2;++i) for(int j=0;j<n;++j) for(int k=0;k<m;++k) a[i][j][k]=read();
for(int i=0;i<1<<(n+m);++i)
for(int j=0;j<2;++j)
if(__builtin_popcount(i)==n||f[i][j]>-1e9)
for(int k=0,t;k<n+m-1;++k)
if((i>>k&3)==1)
t=__builtin_popcount(i&((1<<k)-1)),max(f[i^(3<<k)][j^1],a[j][t][m-1-k+t]-f[i][j]);
printf("%d",f[((1<<n)-1)<<m][1]);
}
IIIDX
Luogu
LOJ
BZOJ
正确的贪心思路:第(1)个点尽可能大,确定完第(1)个点的值后再让第(2)个点的值尽可能大......
先将(d)降序排序,设(f_i)表示第(i)个点左边可用数的数量,初始时(f_i=i)。
我们顺序枚举每个点(i),我们找到最左边的满足(f_pge size_i)((size_i)为(i)的子树大小)的(p),然后找到和(p)位置数值相等的最右边尚未被预定的位置(q),然后把(q)位置给(i)点即(ans_i=q)。
同时我们需要在([1,q))内预定(size_i-1)个数给(i)的子树,那么(forall jin[q,n],f_jleftarrow f_j-size_i)。
同时注意当我们枚举到一个有父亲的节点(i)时,需要把(fa_i)的预定取消,那么(forall jin [ans_{fa_i},n],f_jleftarrow f_j+size_{fa_i}-1)。
线段树维护(f)即可。
维护每个数值最右边的未被选取的位置可以先让每个位置连到数值相等的最右边的位置,再从这个点连到最右边的未被选取的位置即可。
#include<cctype>
#include<cstdio>
#include<algorithm>
const int N=500007;
int n,a[N],next[N],fa[N],size[N],ans[N],t[N*4],tag[N*4];double k;
int read(){int x=0,c=getchar();while(isspace(c))c=getchar();while(isdigit(c))(x*=10)+=c&15,c=getchar();return x;}
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)/2)
void pushup(int p){t[p]=std::min(t[ls],t[rs]);}
void modify(int p,int v){t[p]+=v,tag[p]+=v;}
void pushdown(int p){if(tag[p])modify(ls,tag[p]),modify(rs,tag[p]),tag[p]=0;}
void build(int p,int l,int r)
{
if(l==r) return t[p]=l,void();
build(ls,l,mid),build(rs,mid+1,r),pushup(p);
}
void update(int p,int l,int r,int L,int R,int x)
{
if(L>r||l>R) return ;
if(L<=l&&r<=R) return modify(p,x);
pushdown(p),update(ls,l,mid,L,R,x),update(rs,mid+1,r,L,R,x),pushup(p);
}
int find(int p,int l,int r,int x)
{
if(l==r) return l+(t[p]<x);
return pushdown(p),x<=t[rs]? find(ls,l,mid,x):find(rs,mid+1,r,x);
}
#undef ls
#undef rs
#undef mid
int main()
{
scanf("%d%lf",&n,&k);
for(int i=1;i<=n;++i) a[i]=read();
std::sort(a+1,a+n+1),std::reverse(a+1,a+n+1);
for(int i=n;i;--i) if(a[i]==a[i+1]) next[i]=next[i+1]+1;
for(int i=n;i;--i) size[fa[i]=i/k]+=++size[i];
build(1,1,n);
for(int i=1,p;i<=n;++i)
{
if(fa[i]^fa[i-1]) update(1,1,n,ans[fa[i]],n,size[fa[i]]-1);
p=find(1,1,n,size[i]),p+=next[p],++next[p],p-=next[p]-1,ans[i]=p,update(1,1,n,p,n,-size[i]);
}
for(int i=1;i<=n;++i) printf("%d ",a[ans[i]]);
}
秘密袭击
Luogu
LOJ
BZOJ
我们默认危险值相同的城市中编号大的更危险。
枚举树根(r),计算有多少个以(r)为根的连通块满足第(k)危险的点为(r)。
设(f_{u,i})表示考虑到(u)点时有多少个连通块中(r)的排名为(i)。
初始状态为(f_{r,1}=1)。
对于点(u),如果(u)比(r)危险,那么(f_{u,i}=f_{fa,i-1}),否则(f_{u,i}=f_{fa,i})。
做完(u)的子树之后再(f_{fa,i}leftarrow f_{fa,i}+f_{u,i})。
那么这种情况下对答案的贡献为(f_{r,k}d_r)。
这个做法的时间复杂度为(O(n^2k))
(正解做法是线段树合并维护点值+Lagrange插值)
#include<cstdio>
#include<vector>
#include<cstring>
using u32=unsigned int;
const int N=2007;const u32 P=64123;
int n,k,d[N];u32 f[N][N];std::vector<int>e[N];
int read(){int x;scanf("%d",&x);return x;}
void inc(u32&a,u32 b){if(a+=b,a>=P)a-=P;}
u32 mul(u32 a,u32 b){return a*b%P;}
int cal(int u){int t=0;for(int v=1;v<=n;++v)if(d[v]>d[u]||(d[u]==d[v]&&u>v))++t;return t;}
void dfs(int u,int fa,int root)
{
if(d[root]<d[u]||(d[u]==d[root]&&u<root)) for(int i=1;i<k;++i) inc(f[u][i+1],f[fa][i]); else for(int i=1;i<=k;++i) inc(f[u][i],f[fa][i]);
for(int v:e[u]) if(v^fa) dfs(v,u,root);
for(int i=1;i<=k;++i) inc(f[fa][i],f[u][i]);
}
int main()
{
n=read(),k=read(),read();u32 ans=0;
for(int i=1;i<=n;++i) d[i]=read();
for(int i=1,u,v;i<n;++i) u=read(),v=read(),e[u].push_back(v),e[v].push_back(u);
for(int u=1;u<=n;++u)
{
if(cal(u)<k-1) continue;
memset(f,0,sizeof f),f[u][1]=1;
for(int v:e[u]) dfs(v,u,u);
inc(ans,mul(f[u][k],d[u]));
}
printf("%d",ans);
}
八省联考
劈配
Luogu
LOJ
BZOJ
修改Hungary可以解决第一问。
按排名从前往后到大枚举学员(u),再从优至劣枚举志愿等级,再任意枚举该志愿等级中的导师(x),若(x)的战队名额未满,那么直接让(u)进入(x)的战队。否则按枚举(x)的战队中的学员(v),递归检查能否让(v)选择对他而言和(x)同一等级的导师,如果成功的话就可以让(u)进入(x)的战队。
对于第二问,首先我们判断掉可以进入理想的等级的战队的学员,对于剩下的学员(u),我们给出一个结论:
在枚举(u)的前(s_i)级志愿进行增广时我们会找到一些交错路,让(u)选择交错路中的任何一个学员并升上他的排名都可以让(u)进入理想的等级的战队。
证明:
选择交错路以外的学员一定非法,这个是显然的。
所有交错路一定是(u ightarrow xRightarrow u_0 ightarrow v_0cdots)形式的(单箭头指未匹配边,双箭头指匹配边)。
(u)的前(s_i)志愿等级的所有导师构成的集合就是所有(x)构成的集合,而这些导师的战队中的成员就是(u_0)构成的集合。
如果让(u)上升至任意一个(u_0)的排名,他就可以直接匹配(x)。
如果让(u)上升至任意一个(u_i)的排名,这就相当于是(u)取代了交错路中(u_i)的位置。
那么让(u)匹配(x),并修改(u_j(j<i))匹配(v_j)即可成功匹配。
那么交错路中的学员一定是一个合法的方案,因此选择交错路中排名最低的学员即可。
#include<cctype>
#include<cstdio>
#include<vector>
#include<cstring>
#include<algorithm>
const int N=207;
int n,m,mx,now,b[N],s[N],up[N],id[N],vis[N],bel[N];std::vector<int>a[N][N];
int read(){int x=0,c=getchar();while(isspace(c))c=getchar();while(isdigit(c))(x*=10)+=c&15,c=getchar();return x;}
int dfs(int u,int k)
{
if(u^now) mx=std::max(mx,u);
for(int x:a[u][k])
if(!vis[x])
{
if(vis[x]=1,b[x]) return --b[x],id[u]=k,bel[u]=x;
for(int v=n;v;--v) if(v^u&&bel[v]==x) if(dfs(v,id[v])) return id[u]=k,bel[u]=x;
}
return 0;
}
void solve()
{
n=read(),m=read(),memset(bel+1,0,n*4);
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) a[i][j].clear();
for(int i=1;i<=m;++i) b[i]=read();
for(int i=1;i<=n;++i) for(int j=1;j<=m;++j) a[i][read()].push_back(j);
for(int i=1;i<=n;++i) s[i]=read(),id[i]=m+1,up[i]=i;
for(int i=1;i<=n;++i)
{
memset(vis+1,0,m*4),now=i;
for(int j=1;j<=m;++j)
if(a[i][j].size())
{
if(mx=0,dfs(i,j)) {if(j<=s[i])up[i]=0; break;}
if(j<=s[i]) up[i]=std::min(up[i],i-mx);
}
}
for(int i=1;i<=n;++i) printf("%d ",id[i]);puts("");
for(int i=1;i<=n;++i) printf("%d ",up[i]);puts("");
}
int main(){for(int T=read(),C=read();T;--T)solve();}
林克卡特树
Luogu
LOJ
BZOJ
不难发现题目就是要我们找(k+1)条点不相交的树上路径,使得它们的长度和最大。
一个很简单的贪心做法是每次选择当前的直径,并把直径上的边权取反。
但是这个做法拓展性不强,考虑dp。
设(f_{u,i,0/1/2})表示在(u)的子树中选(i)条链,(u)的度数为(0/1/2)时的答案。
转移分为两种,讨论((u,v))这条边选还是不选即可,时间复杂度为(O(n^2))。
设(g_{u,i}=max{f_{u,i,k}|kin[0,2]}),打表发现(g_{u,i})关于(i)是一个凸函数。
然后凸优化即可。
#include<cctype>
#include<cstdio>
#include<vector>
#include<utility>
#include<algorithm>
using i64=long long;
using pi=std::pair<int,int>;
const int N=300007;
int n,k,cost;std::vector<pi>e[N];
struct node{i64 x;int y;}f[N][3],g[N],t[3];
node operator+(node a,node b){return node{a.x+b.x,a.y+b.y};}
int operator<(node a,node b){return a.x<b.x||(a.x==b.x&&a.y>b.y);}
int read(){int x=0,c=getchar(),f=1;while(isspace(c))c=getchar();if(c=='-')c=getchar(),f=-1;while(isdigit(c))(x*=10)+=c&15,c=getchar();return f*x;}
void dfs(int u,int fa)
{
f[u][0]={0,0},f[u][1]={-cost,1},f[u][2]=g[u]={(i64)-1e18,0};
for(auto[v,w]:e[u])
{
if(v==fa) continue;
dfs(v,u),t[0]=f[u][0],t[1]=f[u][1],t[2]=f[u][2];
for(int i=0;i<3;++i) f[u][i]=std::max(f[u][i],t[i]+g[v]);
f[u][1]=std::max(f[u][1],node{t[0].x+f[v][1].x+w,t[0].y+f[v][1].y}),f[u][2]=std::max(f[u][2],node{t[1].x+f[v][1].x+w+cost,t[1].y+f[v][1].y-1});
}
for(int i=0;i<3;++i) g[u]=std::max(g[u],f[u][i]);
}
int main()
{
n=read(),k=read()+1;int ans=0;
for(int i=1,u,v,w;i<n;++i) u=read(),v=read(),w=read(),e[u].emplace_back(v,w),e[v].emplace_back(u,w);
for(int l=-1e9,r=1e9;l<=r;) cost=(l+r)/2,dfs(1,0),g[1].y<=k? ans=cost,r=cost-1:l=cost+1;
cost=ans,dfs(1,0),printf("%lld",g[1].x+1ll*k*cost);
}
制胡窜
Luogu
LOJ
BZOJ
利用补集容斥将要求的转化为计算两个断点将给定字符串的所有出现位置都切开的方案数。
先建出SAM,然后用线段树合并维护endpos集合。
先排除一种特殊情况:某个端点切开了所有的给定字符串,而另一个端点未切开任何字符串。
我们设给定字符串在原串中出现了(m)次,(pos_i,L_i,R_i)表示给定字符串在原串中第(i)次出现的位置集合/左端点/右端点。
那么剩下来的情况的答案就是(sumlimits_{i=ql}^{qr}|pos_1cap pos_i|*|pos_{i+1}cap pos_m|)。其中(ql,qr)可以在线段树上二分找到。
对于(iin{ql,qr})的情况我们特殊处理,那么剩下的答案为(sumlimits_{i=ql+1}^{i=qr-1}(R_{i+1}-R_i)(R_{i+1}-L_m+1)=sumlimits_{i=ql+1}^{qr-1}(R_{i+1}-R_i)R_{i+1}-(L_m-1)(R_{qr}-R_{ql}+1))。
前面那个求和符号中的式子可以在线段树上额外维护。
#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
using i64=long long;
const int N=200007;
int n,tot,cnt=1,now=1,trans[N][10],link[N],len[N],pos[N],root[N],fa[N][20];char str[N];
struct data{int l,r;i64 sum;};
struct node{int ls,rs;data x;}t[N*20];
data operator+(data a,data b){return data{a.l? a.l:b.l,b.r? b.r:a.r,a.sum+b.sum+(a.r&&b.l? 1ll*b.l*(b.l-a.r):0)};}
int read(){int x=0,c=getchar();while(isspace(c))c=getchar();while(isdigit(c))(x*=10)+=c&15,c=getchar();return x;}
#define mid ((l+r)/2)
void insert(int&p,int l,int r,int x)
{
t[p=++tot].x=data{x,x,0};
if(l^r) x<=mid? insert(t[p].ls,l,mid,x):insert(t[p].rs,mid+1,r,x);
}
int merge(int u,int v)
{
if(!u||!v) return u|v; int p=++tot;
return t[p].ls=merge(t[u].ls,t[v].ls),t[p].rs=merge(t[u].rs,t[v].rs),t[p].x=t[t[p].ls].x+t[t[p].rs].x,p;
}
data query(int p,int l,int r,int L,int R)
{
if(L>r||l>R||L>R) return data{0,0,0};
if(L<=l&&r<=R) return t[p].x;
return query(t[p].ls,l,mid,L,R)+query(t[p].rs,mid+1,r,L,R);
}
int queryl(int p,int l,int r,int x)
{
if(x<=l) return t[p].x.l;
int v; if(x<=mid&&(v=queryl(t[p].ls,l,mid,x))) return v; else return queryl(t[p].rs,mid+1,r,x);
}
int queryr(int p,int l,int r,int x)
{
if(x>=r) return t[p].x.r;
int v; if(x>mid&&(v=queryr(t[p].rs,mid+1,r,x))) return v; else return queryr(t[p].ls,l,mid,x);
}
#undef mid
void extend(int c)
{
int p=now,q;len[now=++cnt]=len[p]+1,pos[len[now]]=now,insert(root[now],1,n,len[now]);
for(;p&&!trans[p][c];p=link[p]) trans[p][c]=now;
if(!p) return link[now]=1,void();
if(len[q=trans[p][c]]==len[p]+1) return link[now]=q,void();
memcpy(trans[++cnt],trans[q],40),len[cnt]=len[p]+1,link[cnt]=link[q],link[q]=link[now]=cnt;
for(;p&&trans[p][c]==q;p=link[p]) trans[p][c]=cnt;
}
void build()
{
static int c[N],a[N];
for(int i=1;i<=n;++i) extend(str[i]&15);
for(int i=1;i<=cnt;++i) ++c[len[i]];
for(int i=1;i<=cnt;++i) c[i]+=c[i-1];
for(int i=cnt;i;--i) a[c[len[i]]--]=i;
for(int i=cnt;i^1;--i) root[link[a[i]]]=merge(root[link[a[i]]],root[a[i]]);
for(int i=1;i<=cnt;++i) fa[i][0]=link[i];
for(int j=1;j<=18;++j) for(int i=1;i<=cnt;++i) fa[i][j]=fa[fa[i][j-1]][j-1];
}
void solve()
{
int l=read(),r=read(),m=r-l+1,p=pos[r];i64 ans=(n-1ll)*(n-2ll)/2;data x,y;
for(int i=18;~i;--i) if(len[fa[p][i]]>=m) p=fa[p][i];
x=t[p=root[p]].x,l=std::max(x.l,queryr(p,1,n,x.r-m+1)),r=std::min(x.r,queryr(p,1,n,x.l+m-1));
if(x.r-x.l<m) ans-=(m-x.r+x.l-1ll)*(n-x.r+x.l-m);
if(l&&r&&l<r) y=query(p,1,n,l,r),ans-=y.sum-(x.r-m+1ll)*(y.r-y.l);
if(r==x.r) ans-=(m-2ll)*(m-x.r+x.l-1)-(m-x.r+x.l-1ll)*(m-x.r+x.l-2ll)/2; else ans-=(m-r+x.l-1ll)*std::max(0,queryl(p,1,n,r+1)-x.r+m-1);
printf("%lld
",ans);
}
int main()
{
int q;n=read(),q=read(),scanf("%s",str+1),build();
while(q--) solve();
}