UNR #1
争夺圣杯
题目描述
解法
比较小清新的签到题,但还是挺有意思的!
首先明确题目其实是想让你分别求出每个 \(m\) 对应的答案是多少。可以考虑贡献法,设点 \(i\) 作为最大值的范围是 \([l_i,r_i]\)(值相同位置小的大),设管辖区间两段较小的长度是 \(mn\),较大的长度是 \(mx\):
- 若 \(m\leq mn\),则贡献是 \(m\cdot a_i\)
- 若 \(mn<m\leq mx\),则贡献是 \(mn\cdot a_i\)
- 若 \(mx<m\leq mx+mn-1\),则贡献是 \((mx+mn-x)\cdot a_i\)
那么可以用差分法维护 \(m\) 对应的系数和常数,时间复杂度 \(O(n)\)
#include <cstdio>
#include <iostream>
using namespace std;
const int M = 1000005;
#define int long long
const int MOD = 998244353;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ans,a[M],s[M],c[M],d[M],L[M],R[M];
signed main()
{
n=read();
for(int i=1;i<=n;i++)
{
a[i]=read();
while(m && a[s[m]]<a[i]) m--;
L[i]=s[m]+1;s[++m]=i;
}
s[m=0]=n+1;
for(int i=n;i>=1;i--)
{
while(m && a[s[m]]<=a[i]) m--;
R[i]=s[m]-1;s[++m]=i;
}
for(int i=1;i<=n;i++)
{
int mn=i-L[i]+1,mx=R[i]-i+1;
if(mn>mx) swap(mn,mx);
c[1]+=a[i];c[mn+1]-=a[i];
c[mx+1]-=a[i];c[mn+mx]+=a[i];
d[mn+1]+=mn*a[i];d[mx+1]-=mn*a[i];
d[mx+1]+=(mn+mx)*a[i];
d[mx+mn]-=(mx+mn)*a[i];
}
for(int i=1;i<=n;i++)
{
c[i]+=c[i-1];d[i]+=d[i-1];
ans^=((c[i]*i)+d[i])%MOD;
}
printf("%lld\n",ans);
}
奇怪的线段树
题目描述
解法
草,傻逼错误调了半个小时,全体目光向我看齐,我宣布个事,我是个傻逼。
言归正传,我们先考虑如何判断无解,发现充要条件是每个黑点的祖先都是黑点,可以做这个判断。
然后我们只需要考虑染黑那些儿子不是黑点的黑点,对于一次染色,我们考察线段树上定位节点的性质:
- 定位节点所代表的区间不交且连续,在树上不存在祖先\(-\)后代关系。
- 定位节点的形式一定是一段连续的右儿子\(+\)一段连续的左儿子(这个只能感性理解,我也不会证明)
- 任意一段连续的右儿子\(+\)一段连续的左儿子都是可被一次区间染色的(可以归纳法证明数量关系)
区间染色等价于选取一段连续的右儿子\(+\)一段连续的左儿子当成定位节点来染色,可以建图表达这个过程。对于所有右儿子 \(i\),我们连向所有 \(l_j=r_i+1\) 的节点 \(j\);对于所有左儿子 \(i\),我们连向所有 \(l_j=r_i+1\) 的左儿子 \(j\)
这样限制被成选取一条路径,有一些点要求必须被经过。那么我们把每个点拆成入点和出点,然后跑网络流,对于有要求的点我们把入点和出点连一条下界为 \(1\) 的边,那么跑有源汇上下界最小流即可。
第二类边是 \(O(n)\) 的,但是第一类边是 \(O(n^2)\) 的,考虑优化建图。我们建立 \(n\) 个辅助节点,首先第 \(l_i\) 个辅助节点连向点 \(i\),对于右儿子 \(i\),我们把 \(i\) 连向第 \(r_i+1\) 个辅助节点,这样我们就只需要跑一个点数和边数都是 \(O(n)\) 级别的网络流。
最后注意,最后要求白色的点不能建入图中,如果一开始没发现这个细节就会向我一样痛苦地调试半个小时。
总结
本题的关键仍然是 \(\tt zkw\) 的哨兵思想,可以结合 线段树 来理解。
#include <cstdio>
#include <cstdlib>
#include <iostream>
#include <queue>
using namespace std;
const int M = 100005;
const int inf = 0x3f3f3f3f;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,cnt,a[M],b[M],L[M],R[M],ls[M],rs[M],pd[M];
int tot=1,ans,ns,nt,S,T,f[M],cur[M],dis[M];
struct edge{int v,c,next;}e[M<<2];
void add(int u,int v,int c)
{
e[++tot]=edge{v,c,f[u]},f[u]=tot;
e[++tot]=edge{u,0,f[v]},f[v]=tot;
}
void init(int x,int l,int r)
{
a[x]=read();L[x]=l;R[x]=r;
if(l==r) {b[x]=a[x];return ;}
int mid=read();
init(ls[x]=++cnt,l,mid);
init(rs[x]=++cnt,mid+1,r);
pd[ls[x]]=1;
if(!a[x] && (a[ls[x]] || a[rs[x]]))
{puts("OwO");exit(0);}
b[x]=a[x] && (!a[ls[x]]) && (!a[rs[x]]);
}
int bfs()
{
for(int i=1;i<=nt;i++) dis[i]=0;
dis[S]=1;queue<int> q;q.push(S);
while(!q.empty())
{
int u=q.front();q.pop();
if(u==T) return 1;
for(int i=f[u];i;i=e[i].next)
{
int v=e[i].v;
if(e[i].c>0 && !dis[v])
dis[v]=dis[u]+1,q.push(v);
}
}
return 0;
}
int dfs(int u,int ept)
{
if(u==T) return ept;
int flow=0,tmp=0;
for(int &i=cur[u];i;i=e[i].next)
{
int v=e[i].v;
if(e[i].c>0 && dis[v]==dis[u]+1)
{
tmp=dfs(v,min(ept,e[i].c));
if(!tmp) continue;
e[i].c-=tmp;e[i^1].c+=tmp;
ept-=tmp;flow+=tmp;
if(!ept) break;
}
}
return flow;
}
signed main()
{
n=read();init(cnt=1,1,n);
S=5*n+1;T=5*n+2;ns=5*n+3;nt=5*n+4;
for(int i=1;i<2*n;i++) if(a[i])//23333
{
add(ns,i,inf);
add(i+2*n,nt,inf);
//
add(i,i+2*n,inf);
if(b[i]) add(i,T,1),add(S,i+2*n,1);
//
add(L[i]+4*n,i,inf);
//
if(!pd[i] && R[i]<n)
add(i+2*n,R[i]+1+4*n,inf);
if(pd[i]) for(int j=i;j<2*n;j++)
if(pd[j] && L[j]==R[i]+1)
add(i+2*n,j,inf);
}
add(nt,ns,inf);
while(bfs())
{
for(int i=1;i<=nt;i++) cur[i]=f[i];
dfs(S,inf);
}
ans=e[tot].c;e[tot].c=e[tot-1].c=0;
S=nt;T=ns;
while(bfs())
{
for(int i=1;i<=nt;i++) cur[i]=f[i];
ans-=dfs(S,inf);
}
printf("%d\n",ans);
}
火车管理
题目描述
解法
根本就不是小清新题!第一次写带标记的主席树给我写傻了!
最难的是二操作,考虑撤回操作其实相当于让这个单点的时间回溯。那么我们用主席树来保存每个时刻的线段树,首先查询覆盖这个单点的时刻是多少,设为 \(x\),然后去时刻 \(x-1\) 查询这个点的栈顶,再更新当前版本的主席树即可。
那么一、三操作都可以直接在主席树上修改和询问。注意主席树的下传也需要新建节点,下传后若节点信息改变需要重新复制,在询问时如果遇到覆盖标记就直接退出,这样就可以卡好新建的点数了。
#include <cstdio>
#include <iostream>
using namespace std;
const int N = 500005;
const int M = 130*N;
int read()
{
int x=0,f=1;char c;
while((c=getchar())<'0' || c>'9') {if(c=='-') f=-1;}
while(c>='0' && c<='9') {x=(x<<3)+(x<<1)+(c^48);c=getchar();}
return x*f;
}
int n,m,ty,ans,cnt,rt[N],a[N],ls[M],rs[M],s[M],fl[M];
void copy(int x,int y)
{
ls[x]=ls[y];rs[x]=rs[y];
s[x]=s[y];fl[x]=fl[y];
}
void down(int x,int l,int r)
{
if(!x || !fl[x]) return ;
copy(++cnt,ls[x]);ls[x]=cnt;
copy(++cnt,rs[x]);rs[x]=cnt;
int mid=(l+r)>>1;
s[ls[x]]=a[fl[x]]*(mid-l+1);
s[rs[x]]=a[fl[x]]*(r-mid);
fl[ls[x]]=fl[rs[x]]=fl[x];fl[x]=0;
}
void add(int &x,int y,int l,int r,int L,int R,int c)
{
copy(x=++cnt,y);
if(L<=l && r<=R)
{
s[x]=(r-l+1)*a[c];
fl[x]=c;return ;
}
int mid=(l+r)>>1;down(y,l,r);copy(x,y);
if(L<=mid) add(ls[x],ls[y],l,mid,L,R,c);
if(mid<R) add(rs[x],rs[y],mid+1,r,L,R,c);
s[x]=s[ls[x]]+s[rs[x]];
}
int ask(int x,int l,int r,int L,int R)
{
if(L>r || l>R) return 0;
if(fl[x]) return (min(r,R)-max(L,l)+1)*a[fl[x]];
if(L<=l && r<=R) return s[x];
int mid=(l+r)>>1;
return ask(ls[x],l,mid,L,R)
+ask(rs[x],mid+1,r,L,R);
}
int get(int x,int l,int r,int id)
{
if(l==r || fl[x]) return fl[x];
int mid=(l+r)>>1;
if(mid>=id) return get(ls[x],l,mid,id);
return get(rs[x],mid+1,r,id);
}
signed main()
{
n=read();m=read();ty=read();
for(int i=1;i<=m;i++)
{
int op=read();rt[i]=rt[i-1];
if(op==2)
{
int x=(read()+ans)%n+1,t=get(rt[i],1,n,x);
if(!t) continue;
t=get(rt[t-1],1,n,x);
add(rt[i],rt[i],1,n,x,x,t);
continue;
}
int l=(read()+ans)%n+1,r=(read()+ans)%n+1;
if(l>r) swap(l,r);
if(op==1)
{
int tmp=ask(rt[i],1,n,l,r);
ans=tmp*ty;printf("%d\n",tmp);
}
else a[i]=read(),add(rt[i],rt[i],1,n,l,r,i);
}
}