JOISC2020

( ext{Day 1})

( ext{ビルの飾り付け 4})

AT
(f_{i,0/1,j})表示考虑前(i)个数,第(i)个数是(A/B),总共有(j)(A)的方案是否存在。
可以证明在固定前两维的情况下合法的第三维下标是一个区间,那么记录左右多点进行转移即可。

#include<cctype>
#include<cstdio>
#include<cstring>
#include<algorithm>
const int N=1000007;
int a[N][2],f[N][2],g[N][2];char ibuf[1<<25],*iS=ibuf,str[N];
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
int main()
{
    fread(ibuf,1,1<<25,stdin);
    int n=read()*2;memset(f+1,-0x3f,8*n),memset(g+1,0x3f,8*n);
    for(int j=0;j<2;++j) for(int i=1;i<=n;++i) a[i][j]=read();
    for(int i=1;i<=n;++i) for(int j=0;j<2;++j) for(int k=0;k<2;++k) if(a[i][j]>=a[i-1][k]) f[i][j]=std::max(f[i][j],f[i-1][k]+j),g[i][j]=std::min(g[i][j],g[i-1][k]+j);
    for(int i=n,k=n/2,x=1e9;i;--i)
    {
	for(int j=0;j<2;++j) if(g[i][j]<=k&&k<=f[i][j]&&x>=a[i][j]) str[i]='A'+j,x=a[i][j],k-=j;
	if(!str[i]) return puts("-1"),0;
    }
    printf("%s",str+1);
}

( ext{掃除})

AT

做法一

先考虑( ext{Sub3})的做法,将所有点按横坐标升序为第一关键字,纵坐标降序为第二关键字排序,那么任何操作都不会修改元素之间的相对顺序。那么用线段树维护排序后的点序列,每次修改在线段树上二分出需要修改的区间,修改操作实际上就是将一个区间内的点的某一维坐标赋值。
然后考虑( ext{Sub4})的做法,注意到一旦一个点被一次修改操作影响到,它和其它被影响到过的点之间的相对顺序就不再变化了。那么用动态开点线段树维护尚未被影响到的点,用平衡树维护被影响到的点即可。
最后考虑正解,用分治消除插入的影响即可。
这个做法常数比较大,但是比做法二常数小一点。

做法二

对于插入操作依旧使用分治消除影响。
先考虑没有向上扫的操作时的做法,此时修改的顺序是无关紧要的,一个点最后的纵坐标是由纵坐标最小的能够扫到它的操作决定的。随便写个什么维护一下就行了。
考虑有了向上扫的操作应该怎么做,先考虑计算每个点最后的横坐标(纵坐标同理),对于一个纵坐标为(len)的向右扫操作,原本它可以将横坐标在([1,n-len])范围内的点扫到(x=n-len)。但是现在可能在这之前有一些向上扫的操作把某些横坐标在([1,x])范围内的点扫到了(y=len)以上,那么这个向右扫的操作就只能影响((x,n-len])的点了。用两棵动态开点线段树维护一下就好了。
这个做法常数比较大。

#include<cctype>
#include<cstdio>
#include<vector>
#include<utility>
#include<algorithm>
#include<functional>
using pi=std::pair<int,int>;
const int N=1500007;
char ibuf[1<<25],*iS=ibuf;
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
int n,m,p,q,tot,root[2],ls[N<<5],rs[N<<5],val[N<<5];pi ans[N];std::vector<pi>vec[2];
struct node{int x,y,t;}a[N];
struct query{int id,l,r;}b[N];
struct change{int o,x;}c[N];
struct Seg_Tree
{
    int ls[N<<5],rs[N<<5],val[N<<5];
#define mid ((l+r)/2)
    void update(int&p,int l,int r,int L,int R,int x)
    {
	if(L>R||l>R||L>r) return ;
	if(!p) p=++tot,ls[p]=rs[p]=val[p]=0;
	if(L<=l&&r<=R) return val[p]=std::max(val[p],x),void();
	update(ls[p],l,mid,L,R,x),update(rs[p],mid+1,r,L,R,x);
    }
    int query(int p,int l,int r,int x)
    {
	if(!p) return 0;
	if(l==r) return val[p];
	return std::max(val[p],x<=mid? query(ls[p],l,mid,x):query(rs[p],mid+1,r,x));
    }
    void modify(int o,int len)
    {
	int t=query(root[!o],0,n,len);
	vec[o].emplace_back(len,t),update(root[o],0,n,t,n-len-1,len+1);
    }
#undef mid
}t;
void solve(int l,int r,std::vector<int>&ask)
{
    std::vector<int>lvc,rvc,now;int mid=(l+r)/2,pos;
    for(int id:ask)
    {
	int L=b[id].l,R=b[id].r;
	if(L<=l&&r<=R) now.push_back(id);
	else
	{
	    if(L<=mid) lvc.push_back(id);
	    if(R>mid) rvc.push_back(id);
	}
    }
    root[0]=root[1]=tot=0,vec[0].clear(),vec[1].clear();
    for(int i=l;i<=r;++i) t.modify(c[i].o,c[i].x);
    std::sort(now.begin(),now.end(),[](int i,int j){return ans[i].second>ans[j].second;}),std::sort(vec[0].begin(),vec[0].end(),std::greater<pi>());
    root[0]=tot=pos=0;
    for(int id:now)
    {
	int x=ans[id].first,y=ans[id].second;
	for(;pos<(int)vec[0].size()&&vec[0][pos].first>=y;++pos) t.update(root[0],0,n,vec[0][pos].second,n-vec[0][pos].first,n-vec[0][pos].first);
	ans[id].first=std::max(x,t.query(root[0],0,n,x));
    }
    std::sort(now.begin(),now.end(),[](int i,int j){return ans[i].first>ans[j].first;}),std::sort(vec[1].begin(),vec[1].end(),std::greater<pi>());
    root[1]=tot=pos=0;
    for(int id:now)
    {
	int x=ans[id].first,y=ans[id].second;
	for(;pos<(int)vec[1].size()&&vec[1][pos].first>=x;++pos) t.update(root[1],0,n,vec[1][pos].second,n-vec[1][pos].first,n-vec[1][pos].first);
	ans[id].second=std::max(y,t.query(root[1],0,n,y));
    }
    if(l!=r) solve(l,mid,lvc),solve(mid+1,r,rvc);
}
int main()
{
    fread(ibuf,1,1<<25,stdin);
    n=read(),m=read();int Q=read();
    for(int i=1;i<=m;++i) a[i]={read(),read(),q+1};
    for(int i=1,x;i<=Q;++i)
	switch(read())
	{
	case 1:x=read(),b[++p]={x,a[x].t,q};break;
	case 2:c[++q]={0,read()};break;
	case 3:c[++q]={1,read()};break;
	case 4:a[++m]={read(),read(),q+1};break;
	}
    std::vector<int>vec;
    for(int i=1;i<=p;++i) if(ans[i]=pi(a[b[i].id].x,a[b[i].id].y),b[i].l<=b[i].r) vec.push_back(i);
    solve(1,q,vec);
    for(int i=1;i<=p;++i) printf("%d %d
",ans[i].first,ans[i].second);
}

做法三

考虑对坐标分治,先处理横纵坐标都在(lfloorfrac n2 floor)范围内的点,然后递归到两个子直角三角形。
修改操作的影响实际上就是将所有点的某维坐标对某个值取(max),和将某维坐标不大于某个值的点移除。
可以用并查集维护两维上的相对顺序,每次取(max)就会将很多点所在的集合合并,只需要记录每个集合中有哪些元素、代表元在该维上的坐标即可。
采用启发式合并可以保证复杂度均摊正确。

#include<queue>
#include<cctype>
#include<cstdio>
#include<vector>
#include<cstring>
#include<utility>
#include<algorithm>
#include<functional>
using pi=std::pair<int,int>;
const int N=1500007;
char ibuf[1<<25],*iS=ibuf;
int n,m,p,q,vis[N];
struct node{int x,y;}a[N];
struct query{int id,x,y;}b[N];
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
struct Disjoint_Set
{
    int fa[N],val[N];std::vector<int>bel[N];
    void init(int x){fa[x]=x,bel[x].clear();if(x)bel[x].push_back(x);}
    int find(int x){return x==fa[x]? x:fa[x]=find(fa[x]);}
    void merge(int u,int v)
    {
	if((u=find(u))==(v=find(v))) return ;
	if(!u) return fa[v]=u,void();
	if(bel[u].size()<bel[v].size()) std::swap(u,v);
	fa[v]=u;for(int w:bel[v]) bel[u].push_back(w);
    }
}tx,ty;
void solve(int sx,int sy,const std::vector<pi>&Q)
{
    if(Q.empty()) return ;
    if(sx+sy==n)
    {
	for(auto it:Q) if(it.first==1) b[it.second].x=a[b[it.second].id].x,b[it.second].y=a[b[it.second].id].y;
	return ;
    }
    int px=(sx+n-sy)/2,py=n-px;std::vector<pi>Qx,Qy;std::priority_queue<pi,std::vector<pi>,std::greater<pi>>qx,qy;
    tx.init(0),ty.init(0);
    for(auto it:Q)
	switch(it.first)
	{
	case 1:
	{
	    int qid=it.second,id=b[qid].id;
	    if(a[id].x>px) Qy.emplace_back(1,qid); else if(a[id].y>py) Qx.emplace_back(1,qid);
	    else b[qid].x=tx.val[tx.find(id)],b[qid].y=ty.val[ty.find(id)];
	    break;
	}
	case 2:
	{
	    int len=it.second;
	    if(len>=py)
	    {
		int now=-1;
		if(len>py) Qx.emplace_back(2,len);
		while(!qx.empty()&&qx.top().first<=n-len)
		{
		    int id=qx.top().second;qx.pop();
		    if(~now) tx.merge(now,id),now=tx.find(now); else now=id;
		}
		if(~now) tx.val[now]=n-len,qx.emplace(n-len,now);
	    }
	    else
	    {
		Qy.emplace_back(2,len);
		while(!qy.empty()&&qy.top().first<=len)
		{
		    int id=qy.top().second;qy.pop();
		    for(int it:ty.bel[id]) if(vis[it]) a[it]={n-len,ty.val[id]},Qy.emplace_back(4,it),vis[it]=0;
		    ty.merge(0,id);
		}
	    }
	    break;
	}
	case 3:
	{
	    int len=it.second;
	    if(len>=px)
	    {
		int now=-1;
		if(len>px) Qy.emplace_back(3,len);
		while(!qy.empty()&&qy.top().first<=n-len)
		{
		    int id=qy.top().second;qy.pop();
		    if(~now) ty.merge(now,id),now=ty.find(now); else now=id;
		}
		if(~now) ty.val[now]=n-len,qy.emplace(n-len,now);
	    }
	    else
	    {
		Qx.emplace_back(3,len);
		while(!qx.empty()&&qx.top().first<=len)
		{
		    int id=qx.top().second;qx.pop();
		    for(int it:tx.bel[id]) if(vis[it]) a[it]={tx.val[id],n-len},Qx.emplace_back(4,it),vis[it]=0;
		    tx.merge(0,id);
		}
	    }
	    break;
	}
	case 4:
	{
	    int id=it.second;
	    if(a[id].x<=px&&a[id].y<=py) tx.init(id),ty.init(id),tx.val[id]=a[id].x,ty.val[id]=a[id].y,qx.emplace(a[id].x,id),qy.emplace(a[id].y,id),vis[id]=1;
	    else if(a[id].x>px) Qy.emplace_back(4,id); else if(a[id].y>py) Qx.emplace_back(4,id);
	    break;
	}
    }
    solve(sx,py+1,Qx),solve(px+1,sy,Qy);
}

int main()
{
    fread(ibuf,1,1<<25,stdin);
    n=read(),m=read(),q=read();std::vector<pi>vec;
    for(int i=1;i<=m;++i) a[i]={read(),read()},vec.emplace_back(4,i);
    for(int i=1;i<=q;++i)
	switch(read())
	{
	case 1:b[++p].id=read(),vec.emplace_back(1,p);break;
	case 2:vec.emplace_back(2,read());break;
	case 3:vec.emplace_back(3,read());break;
	case 4:a[++m]={read(),read()},vec.emplace_back(4,m);
	}
    solve(0,0,vec);
    for(int i=1;i<=p;++i) printf("%d %d
",b[i].x,b[i].y);
}

( ext{Day 2})

( ext{ジョイッターで友だちをつくろう })

AT
假如存在双向边((u,v)),那么(u,v)所在的极大团就会合并。
用并查集维护极大团,set维护各个极大团的单向入边、出边,合并时启发式合并即可。

#include<set>
#include<cctype>
#include<cstdio>
#include<vector>
#include<numeric>
#include<utility>
#include<algorithm>
using pi=std::pair<int,int>;
const int N=100007;
int n,fa[N],size[N];long long ans;char ibuf[1<<22],*iS=ibuf;
std::set<int>in[N];std::set<pi>out[N];
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
int find(int x){return x==fa[x]? x:fa[x]=find(fa[x]);}
void add(int u,int v)
{
    int fu=find(u),fv=find(v);
    if(fu==fv||in[fv].count(u)) return ;
    auto it=out[fv].lower_bound(pi(fu,0));
    if(it==out[fv].end()||it->first!=fu) return ans+=size[fv],in[fv].insert(u),out[fu].emplace(fv,u),void();
    if(in[fu].size()+out[fu].size()<in[fv].size()+out[fv].size()) std::swap(fu,fv);
    std::vector<int>tin;std::vector<pi>tout;
    for(auto it:out[fv]) in[it.first].erase(it.second),ans-=size[it.first],tout.push_back(it);
    for(auto it:in[fv]) find(it),out[fa[it]].erase(pi(fv,it)),tin.push_back(it);
    out[fv].clear(),ans+=2ll*size[fu]*size[fv]-in[fv].size()*size[fv]+in[fu].size()*size[fv],in[fv].clear(),fa[fv]=fu,size[fu]+=size[fv];
    for(auto it:tin) add(it,fu);
    for(auto it:tout) if(it.first!=fu) add(it.second,it.first);
}
int main()
{
    fread(ibuf,1,1<<22,stdin);
    n=read(),std::iota(fa+1,fa+n+1,1),std::fill(size+1,size+n+1,1);
    for(int m=read(),u,v;m;--m) u=read(),v=read(),add(u,v),printf("%lld
",ans);
}

( ext{最古の遺跡 3})

AT
假如给定了一个局面,那么我们可以用如下的算法生成最后留下来的石柱位置集合。
按从大到小枚举高度,将当前高度的石柱加入(S)集合,然后保护(S)集合中位置最靠后的石柱并将其从(S)集合中删去。
(T_i)为高度为(i)的残存石柱的位置,按位置第(i)个残存石柱的高度为(p_i),高度为(i)的石柱的初始位置为(X_i,Y_i(X_i<Y_i))

( ext{Part.1})

先考虑求出(T_1=n+1)时的方案数。
不难发现(T_1=n+1Leftrightarrowforall iin[2,n],T_ige n+2)
也就是说我们需要保证在上述算法的任意一次加入当前高度的石柱之后,(Scap{n+2,cdots,n}=S' evarnothing)
(X_i<n+2le Y_i),那么在第(i)轮操作之后(|S'|)不变,记(a_i=0)
(Y_i<n+2),那么在第(i)轮操作之后(|S'|)会减一,记(a_i=-1)
(n+2le X_i),那么在第(i)轮操作之后(|S'|)会加一,记(a_i=1)
且整个({a_n})(a_i=1)的个数和(a_i=-1)的个数是相等的,记为(m)
(S' evarnothingLeftrightarrowforall iin[2,n],sumlimits_{j=i}^na_ige0),考虑确定({a_n})之后分配(X_i,Y_i)
对于([n+2,2n])的位置,实际上就是把这一侧的石柱的初始高度任意分配,注意到其中有(m)对位置的高度是相同的,因此方案数为(frac{(n-1)!}{2^m})
对于([1,n+1])的位置,除了上面的限制以外还要补上(X_1,Y_1)的限制,因此方案数为(frac{(n+1)!}{2^{m+1}})
总的方案数为

[sumlimits_{2m<n}(n+1)!(n-1)!{n-1choose 2m}frac{C_n}{2^{2m+1}}=(n+1)!(n-1)!frac{C_n}{2^n}=(n-1)!(2n-1)!! ]

( ext{Part.2})

(p_k=min(p_1,cdots,p_k)),那么在第(p_k)轮操作之后,一定有(max{S}<A_k),因此此时(S)中的所有石柱都将被摧毁。
注意初始高度不小于(p_i)的石柱中,各有(j=n-p_i+1)根石柱最终残存/被摧毁。
对所有(min(p_1,cdots,p_i)=p_i)的情况,考虑求出初始高度不小于(p_i)但最终被摧毁的石柱的位置和初始高度,和前(i)根残存石柱的初始高度方案数,记作(f_{i,j})
(min(p_1,cdots,p_{i-1})=n-j+1),若(i=1)(j=0)
(p_i<n-j+1)是前缀最小值,设(p_i=n-j-k+1),那么它会带进来一些初始高度在([n-j-k+1,n-j+1))内的石柱。
考虑确定最终被摧毁的石柱的位置和高度,位置的方案数是简单的,因为这些位置是从(A_i)左边未被占领的位置中任选,因此方案数为({A_i-i-jchoose k})
高度的方案数不太好处理,考虑将初始高度在高度在([n-j-k+1,n-j+1))内的残存石柱一并计算,因为它们都在(A_i)右边,那么我们要求的就是原问题规模为(k)时,(b_1=k+1)的方案数,即((k-1)!(2k-1)!!)。 因为(A_i)右边的任意调换顺序都不会有任何影响,因此若要去掉初始高度在高度在([n-j-k+1,n-j+1))内的残存石柱的影响只需要将上述方案数除以((k-1)!)即可。
也就是说这种情况下的转移为(f_{i,j+k}leftarrow f_{i-1,j}{A_i-i-jchoose k}(2k-1)!!)
(p_ige n-j+1),那么它的初始高度在之前的dp过程中已经被确定了,在(j-i+1)种初始高度中任选一个即可,转移为(f_{i,j}leftarrow(j-i+1)f_{i-1,j})
第一维显然可以滚掉(不过似乎并没有什么卯月)。

#include<cstdio>
using i64=long long;
const int N=1207,P=1000000007;
i64 f[N],fac[N],C[N][N];
int main()
{
    int n;scanf("%d",&n),fac[0]=f[0]=1;
    for(int i=1;i<=n;++i) fac[i]=(2*i-1)*fac[i-1]%P;
    for(int i=0;i<=n;++i) for(int j=C[i][0]=1;j<=i;++j) C[i][j]=(C[i-1][j-1]+C[i-1][j])%P;
    for(int i=1;i<=n;++i)
    {
	int a;scanf("%d",&a);
	for(int j=a-i;~j;--j)
	    if(f[j])
	    {
		for(int k=1;i+j+k<=a;++k) (f[j+k]+=f[j]*fac[k]%P*C[a-i-j][k])%=P;
		f[j]=(j-i+1)*f[j]%P;
	    }
    }
    printf("%lld",f[n]);
}

( ext{Day 3})

( ext{星座 3})

AT
我们将所有楼顶和星星按纵坐标升序排序,然后从小到大枚举纵坐标,处理当前纵坐标的星星和楼顶。
考虑维护对每一列维护当前纵坐标下向左/右最远可以到达哪一列(l/r)(实际上相当于若干个区间);以及若当前纵坐标下该列有一个点,需要使得该点不被涂黑的最小代价(t)
先考虑当前纵坐标的星星。根据(t)我们可以判断该星星是否应该被涂黑。
注意到之后可能还会有其它星星可能与该星星在同一矩形内,即(t_x)随纵坐标增加单调不降,因此可能会出现现在不应该涂黑而之后应该涂黑的反悔情况,但是不可能出现现在应该涂黑以后不应该涂黑的情况。
为了留下反悔的选择,若此时我们应该保留该星星不涂黑,那么我们将当前纵坐标下该星星所在列能够到达的列的(t)全部加上(c-t_x)。((c)是涂黑该星星的代价,(x)是该星星的横坐标)
也就是说对于(t)我们需要支持区间加、单点查询,差分后BIT维护即可。
剩下的就是维护(l,r)了,考虑当前纵坐标下横坐标为(x)的楼顶。
在接下来的纵坐标这个楼顶的阻挡效果将会消失,那么我们将(x)和它两边的区间并在一起即可。这可以使用并查集维护。

#include<cctype>
#include<cstdio>
#include<vector>
#include<numeric>
#include<utility>
using i64=long long;
using pi=std::pair<int,int>;
const int N=200007;
char ibuf[1<<23],*iS=ibuf;
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
std::vector<int>h[N];std::vector<pi>s[N];
struct Disjoint_Set
{
    int fa[N];
    void build(int n){std::iota(fa+1,fa+n+1,1);}
    int find(int x){return x==fa[x]? x:fa[x]=find(fa[x]);}
}l,r;
struct Fenwick_Tree
{
    int n;i64 t[N];
    void build(int n){this->n=n;}
    void add(int p,i64 v){for(;p<=n;p+=p&-p)t[p]+=v;}
    i64 ask(int p){i64 r=0;for(;p;p^=p&-p)r+=t[p];return r;}
}t;
int main()
{
    fread(ibuf,1,1<<23,stdin);
    int n=read();i64 ans=0;
    l.build(n+1),r.build(n+1),t.build(n+1);
    for(int i=1;i<=n;++i) h[read()].push_back(i);
    for(int m=read(),x,y,c;m;--m) x=read(),y=read(),c=read(),s[y].emplace_back(x,c);
    for(int i=1;i<=n;++i)
    {
	for(auto it:s[i])
	{
	    i64 v=t.ask(it.first);
	    if(v>=it.second) ans+=it.second;
	    else ans+=v,t.add(l.find(it.first)+1,it.second-v),t.add(r.find(it.first),v-it.second);
	}
	for(auto it:h[i]) l.fa[it]=it-1,r.fa[it]=it+1;
    }
    printf("%lld",ans);
}

( ext{収穫})

AT
考虑让人不动,树移动。
不难发现当一棵树第一次被采摘之后,它在接下来被哪个人采摘都是完全确定了的。
考虑把上述结构用有向图表示,因为每个人都有恰好一条出边,且沿着出边走一定可以无限地走下去,所以这个有向图是一个基环内向树森林。我们可以认为每棵果树都会向某个叶子节点连一条有向边。
同时我们把询问离线,每个点开一个动态数组来储存。
而基环树森林中的每棵基环树都是相互独立的,因此我们只需要考虑如何计算一棵基环树上的询问的答案。
对于一棵基环树,我们断掉它环上的任意一条边((u,v))(断完之后基环树会变成一棵以(u)为根的有根内向树),然后把答案分成两部分:
(1.)果树到达询问节点时尚未经过被断掉的边。
(2.)果树到达询问节点时已经经过被断掉的边。
第一部分就是查询某个点子树内有多少距离不超过给定值的果树,用dfs序将子树转化为区间,然后把果树和询问按深度升序排序,操作就变成了单点加区间求和,BIT维护即可。
第二部分我们可以考虑先把果树移到(v),设第(i)棵果树移到(v)的时间为(t_i),要回答的询问的时间为(T),基环树的环长为(len)(v)到询问点的距离为(d),那么贡献为(lfloorfrac{T-t_i+len-d}{len} floor)
先把果树和询问按时间升序排序,考虑用(num,sum),分别记录已经扫到的果树的数量和(lfloorfrac{t_i}{len} floor)之和。对于查询(T),先令答案为(lfloorfrac{T+len-d}{len} floor num-sum)
这样(t_imod len>T+len-dmod len)的果树会使得答案多算(1),先把所有余数离散化,剩下的就是单点加后缀查询,BIT即可。

#include<cctype>
#include<cstdio>
#include<vector>
#include<cstring>
#include<utility>
#include<algorithm>
using i64=long long;
using pi=std::pair<i64,i64>;
const int N=200007;
char ibuf[1<<24],*iS=ibuf;
int n,m,L,C,dfn,bu,bv,a[N],next[N],w[N],vis[N],st[N],ed[N];i64 dis[N],ans[N];
struct node{int ty,x,id;i64 tim;node(int a=0,int b=0,int c=0,i64 d=0):ty(a),x(b),id(c),tim(d){}};
int operator<(const node&a,const node&b){return a.tim<b.tim||(a.tim==b.tim&&a.ty<b.ty);}
std::vector<pi>e[N],qry[N];std::vector<int>vec[N];std::vector<node>q1,q2;
struct Fenwick_Tree
{
    int t[3*N],n;
    void init(int n){this->n=n,memset(t,0,4*n+4);}
    void add(int p,int v){for(;p<=n;p+=p&-p)t[p]+=v;}
    int ask(int p){int r=1;for(;p;p^=p&-p)r+=t[p];return r;}
}bit;
i64 read(){i64 x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
void dfs(int u,int fa)
{
    st[u]=++dfn;
    for(auto it:vec[u]) q1.emplace_back(1,st[u],0,dis[u]+it);
    for(auto it:e[u]) if(it.first!=fa&&(u!=bu||it.first!=bv)) dis[it.first]=dis[u]+it.second,dfs(it.first,u),ed[u]=st[it.first];
    ed[u]=dfn;
    for(auto it:qry[u]) q1.emplace_back(2,st[u]-1,it.first,dis[u]+it.second),q1.emplace_back(3,ed[u],it.first,dis[u]+it.second);
}
void solve(int u)
{
    static i64 pos[3*N];int v,cnt=0;i64 len=0,num=0,sum=0;
    while(!vis[u]) vis[u]=1,u=next[u];
    v=next[u],bu=v,bv=u,dfn=0,q1.clear(),dfs(u,0);
    std::sort(q1.begin(),q1.end()),bit.init(dfn),q2.clear();
    for(auto it:q1)
	switch(it.ty)
	{
	case 1:q2.emplace_back(1,0,0,it.tim+w[u]),bit.add(it.x,1);break;
	case 2:ans[it.id]-=bit.ask(it.x);break;
	case 3:ans[it.id]+=bit.ask(it.x);break;
	}
    for(int flg=(u=v,0);!flg||u!=v;len+=w[u],u=next[u],flg=1) for(auto it:qry[u]) if(it.second>=len) q2.emplace_back(2,0,it.first,it.second-len);
    std::sort(q2.begin(),q2.end());
    for(auto&it:q2) it.tim+=(it.ty==2)*len,pos[++cnt]=it.tim%len;
    std::sort(pos+1,pos+cnt+1),cnt=std::unique(pos+1,pos+cnt+1)-pos-1,bit.init(cnt);
    for(auto it:q2)
    {
	int p=std::lower_bound(pos+1,pos+cnt+1,it.tim%len)-pos;
	switch(it.ty)
	{
	case 1:bit.add(p,1),sum+=it.tim/len,++num;break;
	case 2:ans[it.id]+=it.tim/len*num-sum,ans[it.id]-=bit.ask(cnt)-bit.ask(p);break;
	}
    }
}
int main()
{
    fread(ibuf,1,1<<24,stdin);
    auto dis=[](int x,int y){return y-x+(y<x)*L;};
    n=read(),m=read(),L=read(),C=read();
    for(int i=1;i<=n;++i) a[i]=read();
    for(int i=1;i<=n;++i)
    {
	int x=((a[i]-C)%L+L)%L,p=std::upper_bound(a+1,a+n+1,x)-a-1;
	p=!p? n:p,next[i]=p,e[p].emplace_back(i,w[i]=dis(a[p],x)+C);
    }
    for(int i=1;i<=m;++i)
    {
	int b=read(),p=std::upper_bound(a+1,a+n+1,b)-a-1;
	p=!p? n:p,vec[p].push_back(dis(a[p],b));
    }
    m=read();
    for(int i=1,id;i<=m;++i) id=read(),qry[id].emplace_back(i,read());
    for(int i=1;i<=n;++i) if(!st[i]) solve(i);
    for(int i=1;i<=m;++i) printf("%lld
",ans[i]);
}

( ext{Day 4})

( ext{首都})

AT
对于每个城市,我们让它向“要令该城市做首都,必须要需要和该城市合并”的城市连边。
那么最后答案就是城市的图缩点之后最小的出度为(0)的scc的大小减去(1)
实际上一个城市连向的城市,就是树上该城市各个小镇形成的steiner树上的其它城市。
也就是说任意钦定一个点作为根,求出该城市中所有小镇的lca,那么该城市需要连向的城市就是lca到该城市各个小镇的若干路径上的所有城市。
我们让每个小镇向它所在的城市连边,然后重链剖分+线段树优化连边即可。
注意这样建出来的图中一个scc的大小为该scc中的城市点的个数。

#include<cctype>
#include<cstdio>
#include<vector>
#include<algorithm>
const int N=200007,M=5*N;
char ibuf[1<<22],*iS=ibuf;int n,k,m;
std::vector<int>e[N],g[M],vec[N];
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
namespace Tree
{
    int col[N],fa[N],dep[N],size[N],son[N],top[N],dfn[N],pos[N],id[4*N];
    void dfs(int u)
    {
	dep[u]=dep[fa[u]]+1,size[u]=1;
	for(int v:e[u]) if(v^fa[u]) if(fa[v]=u,dfs(v),size[u]+=size[v],size[v]>size[son[u]]) son[u]=v;
    }
    void dfs(int u,int tp)
    {
	static int tim=0;top[u]=tp,pos[dfn[u]=++tim]=u;
	if(son[u]) dfs(son[u],tp);
	for(int v:e[u]) if(v^fa[u]&&v^son[u]) dfs(v,v);
    }
    int lca(int u,int v)
    {
	for(;top[u]^top[v];u=fa[top[u]]) if(dep[top[u]]<dep[top[v]]) std::swap(u,v);
	return dep[u]<dep[v]? u:v;
    }
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)/2)
    void build(int p,int l,int r)
    {
	if(id[p]=++m,l==r) return g[id[p]].push_back(col[pos[l]]);
	build(ls,l,mid),build(rs,mid+1,r),g[id[p]].push_back(id[ls]),g[id[p]].push_back(id[rs]);
    }
    void link(int p,int l,int r,int L,int R,int x)
    {
	if(L>R||l>R||L>r) return ;
	if(L<=l&&r<=R) return g[x].push_back(id[p]);
	link(ls,l,mid,L,R,x),link(rs,mid+1,r,L,R,x);
    }
#undef ls
#undef rs
#undef mid
    void link_chain(int u,int v,int c)
    {
	for(;top[u]^top[v];link(1,1,n,dfn[top[u]],dfn[u],c),u=fa[top[u]]) if(dep[top[u]]<dep[top[v]]) std::swap(u,v);
	link(1,1,n,std::min(dfn[u],dfn[v]),std::max(dfn[u],dfn[v]),c);
    }
    void build()
    {
	n=read(),m=k=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 i=1;i<=n;++i) col[i]=read();
	dfs(1),dfs(1,1),build(1,1,n);
	for(int i=1;i<=n;++i) vec[col[i]].push_back(i);
	for(int i=1;i<=k;++i)
	{
	    if(!vec[i].size()) continue;
	    int p=vec[i][0];
	    for(int u:vec[i]) p=lca(p,u);
	    for(int u:vec[i]) link_chain(u,p,i);
	}
    }
}
namespace Graph
{
    int cnt,dfn[M],low[M],ins[M],stk[M],bel[M],deg[M],size[M];
    void tarjan(int u)
    {
	static int tim,top;
	dfn[u]=low[u]=++tim,stk[++top]=u,ins[u]=1;
	for(int v:g[u]) if(!dfn[v]) tarjan(v),low[u]=std::min(low[u],low[v]); else if(ins[v]) low[u]=std::min(low[u],dfn[v]);
	if(dfn[u]==low[u]) for(++cnt;stk[top+1]!=u;bel[stk[top]]=cnt,size[cnt]+=stk[top]<=k,ins[stk[top--]]=u);
    }
    void solve()
    {
	int ans=k-1;
	for(int i=1;i<=m;++i) if(!dfn[i]) tarjan(i);
	for(int u=1;u<=m;++u) for(int v:g[u]) if(bel[u]^bel[v]) ++deg[bel[u]];
	for(int i=1;i<=cnt;++i) if(!deg[i]) ans=std::min(ans,size[i]-1);
	printf("%d",ans);
    }
}
int main()
{
    fread(ibuf,1,1<<22,stdin);
    Tree::build();
    Graph::solve();
}

( ext{治療計画})

AT
(f_i)表示在采用第(i)个计划的前提下,在(t_i)时刻治疗了([1,r_i])的最小花费。
初始状态为

[f_i=egin{cases}+infty&l_i e1\c_i&l_i=1end{cases} ]

然后(f_i)能够转移到(f_j)的充要条件是(r_i-l_j+1ge|t_i-t_j|)
如果可以转移的话那么就是(f_i+c_j ightarrow f_j)
不难发现这个dp是最短路的形式,考虑模拟dijkstra的过程。
最开始将所有计划按时间排序,那么能够转移的充要条件就是

[egin{cases}r_i-t_i+1ge l_j-t_j&jin[1,i)\r_i+t_i+1ge l_j+t_j&jin(i,m]end{cases} ]

同时我们注意到一个点的入边边权都是相同的,因此一个点只会被更新一次。
那么把上面两个限制条件分开,用线段树维护右边的区间最小值,每次在线段树上暴力递归更新,更新完之后设成(+infty)即可。

#include<queue>
#include<cctype>
#include<cstdio>
#include<utility>
#include<algorithm>
#include<functional>
using i64=long long;
using pi=std::pair<int,int>;
const int N=100007,inf=2e9;
char ibuf[1<<22],*iS=ibuf;i64 f[N],t[4*N][2];
struct node{int t,l,r,c;}a[N];
struct data{i64 w;int u;data(i64 a=0,int b=0):w(a),u(b){}};
int operator<(const data&a,const data&b){return a.w>b.w;}
std::priority_queue<data>q;
int read(){int x=0;while(isspace(*iS))++iS;while(isdigit(*iS))(x*=10)+=*iS++&15;return x;}
#define ls p<<1
#define rs p<<1|1
#define mid ((l+r)/2)
void pushup(int p){for(int i=0;i<2;++i)t[p][i]=std::min(t[ls][i],t[rs][i]);}
void insert(int p,int l,int r,int x,int v,int w)
{
    if(l==r) return t[p][0]=v,t[p][1]=w,void();
    x<=mid? insert(ls,l,mid,x,v,w):insert(rs,mid+1,r,x,v,w),pushup(p);
}
void query(int p,int l,int r,int L,int R,int lim,int o,i64 dis)
{
    if(L>r||l>R||L>R||lim<t[p][o]) return ;
    if(l==r) return t[p][0]=t[p][1]=inf,q.emplace(f[l]=a[l].c+dis,l);
    query(ls,l,mid,L,R,lim,o,dis),query(rs,mid+1,r,L,R,lim,o,dis),pushup(p);
}
#undef ls
#undef rs
#undef mid
int main()
{
    fread(ibuf,1,1<<22,stdin);
    int n=read(),m=read();i64 ans=1e18;
    for(int i=1;i<=m;++i) a[i]={read(),read(),read(),read()};
    std::sort(a+1,a+m+1,[](const node&a,const node&b){return a.t<b.t;});
    for(int i=1;i<=m;++i) if(a[i].l==1) q.emplace(f[i]=a[i].c,i),insert(1,1,m,i,inf,inf); else f[i]=1e18,insert(1,1,m,i,a[i].l-a[i].t,a[i].l+a[i].t);
    while(!q.empty())
    {
	i64 d=q.top().w;int u=q.top().u;q.pop();
	query(1,1,m,1,u-1,a[u].r-a[u].t+1,0,d),query(1,1,m,u+1,m,a[u].r+a[u].t+1,1,d);
    }
    for(int i=1;i<=m;++i) if(a[i].r==n) ans=std::min(ans,f[i]);
    printf("%lld",ans==(i64)1e18? -1:ans);
}
原文地址:https://www.cnblogs.com/cjoierShiina-Mashiro/p/13025145.html