lp2468 SDOI2010 粟粟的书架

简化题意。
有一个n*m的矩阵,q个询问,求问,在每个矩阵中最少取多少个数,使得和至少为h。
观察数据范围,我们惊讶地发现这一题不可避免地要做两次。
因为,n,m<=200显然不是用普通的数据结构可以维护的。我们考虑使用\(cnt_{i,j,k}\)表示大于k的数的数量,\(sm_{i,j,k}\)表示大于k的数的总和。然后二分这个k来求解即可。
对于n==1的情况,我们很容易可以想到维护一棵权值主席树。对于每一个询问,我们在两棵权值线段树上二分然后求差。

#include<iostream>
#include<cstdio>
#define MID ((L+R)>>1)
int n,m,q;
int b[205][205],cnt[205][205][1005],sm[205][205][1005];
//不需要开longlong!!!MLE警告!!! 

inline long long cntQry(int X1,int Y1,int X2,int Y2,int K){
    return cnt[X2][Y2][K]-cnt[X1-1][Y2][K]-cnt[X2][Y1-1][K]+cnt[X1-1][Y1-1][K];
}
inline long long smQry(int X1,int Y1,int X2,int Y2,int K){
    return sm[X2][Y2][K]-sm[X1-1][Y2][K]-sm[X2][Y1-1][K]+sm[X1-1][Y1-1][K];
}
inline void slv2(){
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            scanf("%d",&b[i][j]);
        }
    }
    for(int k=0;k<=1000;++k){
        for(int i=1;i<=n;++i){
            for(int j=1;j<=m;++j){
                cnt[i][j][k]=cnt[i-1][j][k]+cnt[i][j-1][k]-cnt[i-1][j-1][k]+(int)(b[i][j]>=k);
                sm[i][j][k]=sm[i-1][j][k]+sm[i][j-1][k]-sm[i-1][j-1][k]+(int)(b[i][j]>=k)*b[i][j];
            }
        }
    }
    long long l,r,mid,h,ans;
    int X1,X2,Y1,Y2;
    while(q--){
        l=0,r=1001,ans=-1;
        scanf("%d%d%d%d%lld",&X1,&Y1,&X2,&Y2,&h);
        while(l<=r){
            mid=(l+r)>>1,smQry(X1,Y1,X2,Y2,mid)>=h?(ans=mid,l=mid+1):r=mid-1;
        }
        (~ans)?(printf("%lld\n",cntQry(X1,Y1,X2,Y2,ans)-(smQry(X1,Y1,X2,Y2,ans)-h)/ans)):(puts("Poor QLW"));
    }
    
//	 对于同一个k可能有多个值,而可能只需要选取部分。 
}
const int MAXN2=10000005;
int a[MAXN2];
//数组大小别算错 
class ChairmanTree{
    private:
        class Node{
            public:
                int l;
                int r;
                int fa;
                int sz;
                int sm;
        };
        Node tr[MAXN2];
        int cnt,rt[MAXN2];
        inline void build(int LST,int &NW,int L,int R,int V){
            NW=++cnt;tr[NW].sz=tr[LST].sz+1;tr[NW].sm=tr[LST].sm+V;
            if(L==R){return;}
            MID>=V?(build(tr[LST].l,tr[NW].l,L,MID,V),tr[NW].r=tr[LST].r):(build(tr[LST].r,tr[NW].r,MID+1,R,V),tr[NW].l=tr[LST].l);
            //如果值小等于MID。那么就更新右边的值;否则更新左边的值。一定要注意判断V==MID时的情况。 
            //如果更新的是右边的值,那么就把右边的节点与上一个版本的右边的节点一并下传,并修改本节点的左节点;否则反之。 
        }
        //从根往下搜索。注意减去的应当是右节点的值。 
        inline int qry(int A,int B,int L,int R,int V){
            int RT=0;
            while(L<R){(tr[tr[B].r].sm-tr[tr[A].r].sm)>V?(L=MID+1,B=tr[B].r,A=tr[A].r):(RT+=tr[tr[B].r].sz-tr[tr[A].r].sz,V-=(tr[tr[B].r].sm-tr[tr[A].r].sm),R=MID,B=tr[B].l,A=tr[A].l);}
            //同理,对于等于的情况也应当特别注意。 
            return RT+(V+L-1)/(L);
            //剩下的部分应该全部由大小为R的这些东西,也就是当前点的值来处理掉。故而要加上(V+L-1)/(L)
        }
    public:
        inline void ADD(int VER,int X){
            build(rt[VER-1],rt[VER],1,1000,X);
        }
        inline int QUREY(int LVER,int RVER,int X){
            return qry(rt[LVER-1],rt[RVER],1,1000,X);
        }
        inline int SUM(int L,int R){
            return tr[rt[R]].sm-tr[rt[L-1]].sm;
        }
        inline void INIT(){
            cnt=0;
        }
};
ChairmanTree T;
inline void slv1(){
    T.INIT();
    for(int i=1;i<=m;++i){
        scanf("%d",&a[i]);
        T.ADD(i,a[i]);
    }
    int l,r,tmp,x;
    while(q--){
        scanf("%d%d%d%d%d",&tmp,&l,&tmp,&r,&x);
        if(T.SUM(l,r)<x){
            puts("Poor QLW");
            continue;
        }
        printf("%d\n",T.QUREY(l,r,x));
    }
}

void init(){
    scanf("%d%d%d",&n,&m,&q);
    n==1?slv1():slv2();
}

int main(){
    init();
    return 0;
} 

lp1131 ZJOI2007 时态同步

有一棵树,每次将一条边的长度增加1的代价为1,求,使得所有叶节点到根节点距离相同的最小代价。
很显然,对于一棵树来说,贪心地考虑,最终的距离显然是最开始的最远叶节点的距离。
然后,继续贪心地考虑,每一次增加的代价,应当避免那些已经是最远距离的节点。
但是这样做的话复杂度最坏是n^2的。
故而我们考虑树形DP。对于每个节点临时记以这个节点为根的子树完成时态同步需要的代价。然后每一次修改把修改结果记下来即可。

#include<iostream>
#include<cstdio>

inline int Max(int A,int B){
	return A>B?A:B;
}
struct ee{
	int v;
	int w;
	int nxt;
}e[1000005];
int h[500005],et=0;
inline void add(int U,int V,int W){
	e[++et]=(ee){V,W,h[U]};
	h[U]=et;
}
int n,s,f[500005];
long long ans=0;
inline void dfs(int X,int FA){
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==FA){
			continue;
		}
		dfs(e[i].v,X);
	}
	int mx=0;
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==FA){
			continue;
		}
		mx=Max(mx,f[e[i].v]+e[i].w);
	}
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==FA){
			continue;
		}
		ans+=mx-(f[e[i].v]+e[i].w);
	}
	f[X]=mx;
}
void init(){
	scanf("%d%d",&n,&s);
	int u,v,w;
	for(int i=1;i<n;++i){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
		add(v,u,w);
	}
	dfs(s,0);
	printf("%lld\n",ans);
}

int main(){
	init();
	return 0;
}

lp2150 NOI2015 寿司晚宴


将2~n的整数划分到两个集合,使得两个集合中任意两个元素互质,求方案数。
对于n<=30的情况是很容易想到的,将每个数\(i\)求出它的质因子集合\(K_{i}\)。
很容易可以发现,30以内的质数仅有10种。故而我们就有了一个\(O(2^{202}n)\)的做法。
对于n<=500的情况,似乎用上述的做法并不是十分可行,无论是时间复杂度还是空间复杂度都是不可接受的。
但是我们发现,有且仅有7个质数,在500以内会作为因数出现多次。我们是不是可以用一种奇技淫巧把大于19的质数处理掉呢?
我们惊讶地发现,由于所有较大的质数会且只会出现一次,那么很显然所有最大质因子是较大质数且最大质因子相同的数只能放到一个集合里。
故而我们把所有数按照最大质因子排序,然后从小到大遍历。当最大质因子成为较大质数的时候,不再使用原来的数组,而是将「这个质因子一个都没选」的情况复制到两个新的数组f2[2][1<<7][1<<7]。
每一次更换最大质因子的时候都从原数组继承DP值,然后在这里面对每一个是放在A还是放在B的情况来更新:

$$f_{S_1,S_2} = f2_{0,S_1,S_2} + f2_{1,S_1,S_2} – f_{S_1,S_2}$$

之所以最后还要减去原来的值是因为,两个新数组的最终结果各自都是从原来的「这个质因子一个都没选」的情况复制过来的,因此「这个质因子一个都没选」的情况就会被统计两次。


#include<iostream>
#include<cstdio>
#include<algorithm>
/*

*/ 
const int P[8]={2,3,5,7,11,13,17,19};
const int MAX=1<<8;
long long MOD;
long long f[MAX][MAX],f2[2][MAX][MAX];
struct data{
	int v;
	int big;
	int K;
	inline void init(int X){
		v=X,big=0;
		int nw=v;K=0;
		for(int i=0;i<8;++i){
			if(!(nw%P[i])){
				K|=(1<<i);
			}
			while(!(nw%P[i])){
				nw/=P[i];
			}
		}
		if(nw>1){
			big=nw;
		}
	}
	inline bool operator<(const data &B)const{
		return big<B.big;
	}
}a[505];
int n;
void init(){
	scanf("%d%lld",&n,&MOD);
	--n;
	for(int i=1;i<=n;++i){
		a[i].init(i+1);
	}
	std::sort(a+1,a+1+n);
	int lst=0x3f3f3f3f;
	f[0][0]=1;
	for(int i=1;i<=n;++i){
		if(a[i].big){
			lst=i;
			break;
		}
		for(int j=MAX-1;~j;--j){
//			注意这里要-1 
			for(int k=MAX-1;~k;--k){
				f[j][k|a[i].K]+=f[j][k];f[j|a[i].K][k]+=f[j][k];
				f[j][k|a[i].K]%=MOD;f[j|a[i].K][k]%=MOD;
			}
		}
	}
	for(int i=lst;i<=n;++i){
		if(a[i].big!=a[i-1].big){
			for(int j=0;j<MAX;++j){
				for(int k=0;k<MAX;++k){
					f2[0][j][k]=f2[1][j][k]=f[j][k];
				}
			}
		}
		for(int j=MAX-1;~j;--j){
			for(int k=MAX-1;~k;--k){
				if(j&k){
					continue;
				}
				if(!(k&a[i].K)){
					f2[0][j|a[i].K][k]+=f2[0][j][k];
					f2[0][j|a[i].K][k]%=MOD;
				}
				if(!(j&a[i].K)){
					f2[1][j][k|a[i].K]+=f2[1][j][k];
					f2[1][j][k|a[i].K]%=MOD;
				}
			}
		}
		if(a[i].big!=a[i+1].big){
			for(int j=0;j<MAX;++j){
				for(int k=0;k<MAX;++k){
					f[j][k]=(f2[0][j][k]+f2[1][j][k]+(MOD-f[j][k]))%MOD;
				}
			}
		}
	}
	long long ans=0;
	for(int i=MAX-1;~i;--i){
		for(int j=MAX-1;~j;--j){
			if(i&j){
				continue;
			}
			ans+=f[i][j];
			ans%=MOD;
		}
	}
	printf("%lld\n",ans);
}

int main(){
	init();
	return 0;
}

CF1099

一场爆肝场。
如果不是划水划到太晚才不会打呢(大雾)
整体来说难度较低…但是EF都没做出来,丢人。


CF1099A
一道简单题。直接处理贡献即可。


#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;

int w,h;
int d1,w1,d2,w2;
void init(){
	scanf("%d%d%d%d%d%d",&w,&h,&w1,&d1,&w2,&d2);
	int ans=w;
	for(int i=h;i>=0;--i){
		ans+=i;
		if(d1==i){
			ans-=w1;
			ans=std::max(ans,0);
			continue;
		}
		if(d2==i){
			ans-=w2;
			ans=std::max(ans,0);
			continue;
		}
		
	}
	printf("%d\n",ans);
}
int main(){
	init();
	return 0;
}


CF1099B
找规律题。
解一个二次方程很容易就可以找到最优解。
实际情况当然是大力分类讨论。


#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<cmath>
using namespace std;
int n;
void init(){
	int ans=0;
	scanf("%d",&n);
	int flr=(floor(sqrt(n)));
	ans=2*flr;
	n-=flr*flr;
	if(n&&n<=flr){
		++ans;
	}else if(n&&n>flr){
		ans+=2;
	}
	printf("%d\n",ans);
}
int main(){
	init();
	return 0;
}


CF1099C
一道有一定难度的模拟题。
很容易可以想到朴素的贪心做法。就是,能删的全都删;要加的一次加光。
要考虑很多细节。注意对无解情况的判定。


#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
char base[205];
int len,del,ad,gl;
void init(){
	cin>>base+1;
	scanf("%d",&gl);
	len=strlen(base+1);
	for(int i=1;i<=len;++i){
		if(base[i]=='?'){
			++del;
		}
		if(base[i]=='*'){
			++ad;
		}
	}
	int n=len;
	len-=del;
	len-=ad;
	if(len-del-ad>gl||(!ad&&len<gl)){
		puts("Impossible");
	}else{
		if(len==gl){
			for(int i=1;i<=n;++i){
				if(base[i]!='?'&&base[i]!='*'){
					putchar(base[i]);
				}
			}
		}else if(len>gl){
			int cnt=len-gl;
			for(int i=1;i<=n;++i){
				if(base[i]=='*'){
					base[i]='?';
				}
			}
			for(int i=1;i<=n;++i){
				if(base[i+1]=='?'&&cnt){
					++i;
					--cnt;
				}else if(base[i]=='?'){
					continue;
				}else{
					putchar(base[i]);
				}
			}
			return;
		}else if(len<gl){
			int cnt=gl-len;
			for(int i=1;i<=n;++i){
				if(base[i+1]=='*'&&cnt){
					for(int j=0;j<=cnt;++j){
						putchar(base[i]);
					}
					cnt=0;
				}else if(base[i]=='?'||base[i]=='*'){
					continue;
				}else{
					putchar(base[i]);
				}
			}
		}
	}
}
int main(){
	init();
	return 0;
}


CF1099D
一道有一定思维难度的树形DP。
很容易可以证明,贪心地取子树最小总是更优的;下方只要不比上方大那就始终是有解的。


#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
struct ee{
	int v;
	int nxt;
}e[200005];
int h[100005],et=0,fa[100005],dep[100005];
inline void add(int U,int V){
	e[++et]=(ee){V,h[U]};
	h[U]=et;
}
int n,s[100005],a[100005];
inline void dfs0(int X){
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==fa[X]){
			continue;
		}
		dep[e[i].v]=dep[X]+1;
		dfs0(e[i].v);
	}
}
inline void dfs(int X){
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==fa[X]){
			continue;
		}
		dfs(e[i].v);
	}
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v==fa[X]){
			continue;
		}
		if(!(dep[X]&1)){
			s[X]=std::min(s[X],s[e[i].v]);
		}
	}
	if(s[X]==0x3f3f3f3f){
		s[X]=s[fa[X]];
	}
}
void init(){
	scanf("%d",&n);
	int v;
	for(int i=2;i<=n;++i){
		scanf("%d",&v);
		fa[i]=v;
		add(i,v);
		add(v,i);
	}
	dep[1]=1,fa[1]=0;
	dfs0(1);
	for(int i=1;i<=n;++i){
		scanf("%d",s+i);
	}
	for(int i=1;i<=n;++i){
		if(s[i]==-1){
			s[i]=0x3f3f3f3f;
		}
	}
	dfs(1);
//	for(int i=1;i<=n;++i){
//		printf("%d ",dep[i]);
//	}
//	puts("");
	long long ans=0;
	for(int i=1;i<=n;++i){
		a[i]=s[i]-s[fa[i]];
		ans+=a[i];
		if(a[i]<0){
			puts("-1");
			return;
		}
	}
	printf("%I64d\n",ans);
}
int main(){
	init();
	return 0;
}

lp2766 网络流24题-最长不下降子序列问题

这是一道看起来很简单的麻题。
很显然可以发现第一问就是直接n^2DP求一个答案。
第二问模拟寻找最长不下降子序列的过程。
第三问改几条边再跑一遍。
但是要注意很多很多细节。
比如说,建模的时候应该反着建。
再比如说,对于最长长度只有1的情况应当格外注意,因为这可能会导致各种错误。
写起来还是有点烦心的。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>

const int INF=0x3f3f3f3f;
const int BIG=19260817;
inline int Min(int A,int B){
	return A<B?A:B;
}

inline int Max(int A,int B){
	return A>B?A:B;
}

struct ee{
	int v;
	int w;
	int nxt;
}e[250005];
int h[2005],et=-1;

inline void Eadd(int U,int V,int W){
	e[++et]=(ee){V,W,h[U]};
	h[U]=et; 
}
inline void add(int U,int V,int W){
	Eadd(U,V,W);
	Eadd(V,U,0);
}

int dep[2005],nw[2005],a[505];
int n,s,t,len;
std::queue<int> q;
inline bool bfs(){
	for(int i=1;i<=t;++i){
		dep[i]=INF,nw[i]=h[i];
	}
	dep[s]=1;
	while(!q.empty()){
		q.pop();
	}
	q.push(s);
	int p;
	while(!q.empty()){
		p=q.front();
		q.pop();
		for(int i=h[p];i>=0;i=e[i].nxt){
			if(dep[e[i].v]==INF&&e[i].w){
				dep[e[i].v]=dep[p]+1;
				q.push(e[i].v);
			}
		}
	}
//	for(int i=1;i<=t;++i){
//		printf("%d ",dep[i]);
//	}
//	puts("");
	return dep[t]^INF;
}

inline int dfs(int X,int V){
	if(!V||X==t){
		return V;
	}
	int cnt=0,val;
	for(int i=nw[X];i>=0;i=e[i].nxt){
		nw[X]=i;
		if(dep[e[i].v]==dep[X]+1){
			val=dfs(e[i].v,Min(V,e[i].w));
			if(val){
				cnt+=val;
				V-=val;
				e[i].w-=val;
				e[i^1].w+=val;
				if(!V){
					break;
				}
			}
		}
	}
	return cnt;
}
int f[505];
inline void slv1(){
	for(int i=1;i<=n;++i){
		f[i]=1;
		for(int j=1;j<i;++j){
			if(a[i]>=a[j]){
				f[i]=Max(f[i],f[j]+1);
			}
		}
	}
	int ans=0;
	for(int i=1;i<=n;++i){
		ans=Max(ans,f[i]);
	}
	len=ans;
	printf("%d\n",ans);
}

inline int dinic(){
	int RT=0;
	while(bfs()){
		RT+=dfs(s,INF);
	}
	return RT;
}

inline void slv2(){
	s=2*n+1,t=2*n+2;
	for(int i=1;i<=t;++i){
		h[i]=-1;
	}
	et=-1;
	for(int i=1;i<=n;++i){
		if(f[i]==len){
			add(i+n,t,1);
		}
		if(f[i]==1){
			add(s,i,1);
		}
		add(i,i+n,1);
		for(int j=1;j<i;++j){
			if(a[i]>=a[j]&&f[j]+1==f[i]){
				add(j+n,i,1);
			}
		}
	}
	int ans=0;
	while(bfs()){
		ans+=dfs(s,INF);
	}
	printf("%d\n",ans);
	add(1,n+1,BIG),add(s,1,BIG);
	if(f[n]==len){
		add(n,2*n,BIG),add(2*n,t,BIG);
	}
	while(bfs()){
		ans+=dfs(s,INF);
	}
	printf("%d\n",ans);
}

void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",a+i);
	}
	slv1();
	slv2();
}
int main(){
	init();
	return 0;
}

lp1963 NOI2009 变换序列

如果知道这道题是一个二分图匹配,就显得非常简单了。
首先我们对原序列和变换序列各自建一个点。
然后,对于每一组约束条件\(D_{i}\),我们向所有到\(i\)的距离为\(D_{i}\)的点连一条边。
然后先跑一遍二分图最大匹配。如果全部都能匹配上就说明有解。

但是题目要求字典序输出这组解,怎么办呢?
这么做的意思就是,对于一个尽可能小的数\(i\),它匹配的数\(T_{i}\)也要尽可能小。
故而,搜索的时候从大往小即可。

#include<iostream>
#include<cstdio>
int n,D[10005];
int vis[10005],dep=0;
int usd[10005];

inline bool dfs(int X){
	int up=(X+D[X]-1)%n+1,dw=(X-D[X]+n-1)%n+1;
	if(up<dw){
		std::swap(up,dw);
	}
//	printf("%d %d\n",up,dw);
	if(!usd[dw]&&vis[dw]!=dep){
		vis[dw]=dep;
		usd[dw]=X;
		return 1;
	}
	if(vis[dw]!=dep){
		vis[dw]=dep;
		if(dfs(usd[dw])){
			usd[dw]=X;
			return 1;
		}
	}
	if(!usd[up]&&vis[up]!=dep){
		vis[up]=dep;
		usd[up]=X;
		return 1;
	}
	if(vis[up]!=dep){
		vis[up]=dep;
		if(dfs(usd[up])){
			usd[up]=X;
			return 1;
		}
	}
	
	return 0;
}
int Ans[10005];
void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&D[i]);
	}
	int ans=0;
	for(int i=n;i>=1;--i){
		++dep;
		if(dfs(i)){
			++ans;
		}else{
			break;
		}
	}
	if(ans<n){
		puts("No Answer");
	}else{
		for(int i=1;i<=n;++i){
			Ans[usd[i]]=i;
		}
		for(int i=1;i<=n;++i){
			printf("%d ",Ans[i]-1);
		}
	}
}

int main(){
	init();
	return 0;
}

lp2764 网络流24题-最小路径覆盖问题


(其实建图方法已经写在题目里了。)
(这一题是一道匈牙利的裸题,考场上二分图最大匹配仍然建议使用匈牙利(因为在实现复杂度上有巨大优势),我纯粹练一下当前弧优化Dinic。)
首先有一个定理。将一张有向图上的每一个点都拆点成入点和出点,连边后形成一张二分图,那么原图的最小路径覆盖,就是顶点数减去这张二分图的最大匹配。
考虑感性地证明这个定理。
每一次匹配,事实上就意味着,一条路径可以额外走过一个点。这样就减少了一个路径覆盖。
所以,当匹配次数最大的时候,路径覆盖数就最小了。
答案用并查集处理一下输出。


#include<iostream>
#include<cstdio>
#include<queue>
const int INF=0x3f3f3f3f;

inline int Min(int A,int B){
	return A<B?A:B;
}

struct ee{
	int v;
	int w;
	int nxt;
}e[20005];
int h[2005],et=-1;
inline void Eadd(int U,int V,int W){
	e[++et]=(ee){V,W,h[U]};
	h[U]=et;
}
inline void add(int U,int V,int W){
	Eadd(U,V,W);
	Eadd(V,U,0);
}
int dep[2005],nw[2005],f[2005];
int n,m,s,t;

std::queue<int> q;
inline bool bfs(){
	for(int i=1;i<=t;++i){
		dep[i]=INF;
		nw[i]=h[i];
	}
	while(!q.empty()){
		q.pop();
	}
	dep[s]=0;
	q.push(s);
	int p;
	while(!q.empty()){
		p=q.front();
		q.pop();
		for(int i=h[p];i>=0;i=e[i].nxt){
			if(dep[e[i].v]==INF&&e[i].w){
				dep[e[i].v]=dep[p]+1;
				q.push(e[i].v);
			}
		}
	}
	return dep[t]<INF;
}
inline int dfs(int X,int V){
	if(V<=0||X==t){
		return V;
	}
	int cnt=0,val;
	for(int i=nw[X];i>=0;i=e[i].nxt){
		nw[X]=i; 
		if(dep[e[i].v]==dep[X]+1){
			val=dfs(e[i].v,Min(e[i].w,V));
			if(val){
				cnt+=val;
				V-=val;
				e[i].w-=val;
				e[i^1].w+=val;
				if(V<=0){
					break;
				}
			}
		}
	}
	return cnt;
}

inline int fnd(int X){
	for(int i=h[X+n];i>=0;i=e[i].nxt){
		if(1<=e[i].v&&e[i].v<=n){
			if(e[i].w){
				return e[i].v;
			}
		}
	}
	return X;
}
inline int fa(int X){
	return f[X]^X?f[X]=fa(f[X]):X;
}
inline void prnt(int X){
	for(int i=h[X];i>=0;i=e[i].nxt){
		if((n+1)<=e[i].v&&e[i].v<=(n<<1)&&!e[i].w){
			printf("%d ",e[i].v-n
			);
			prnt(e[i].v-n);
			return;
		}
	}
}
bool vis[2005];
void init(){
	scanf("%d%d",&n,&m);
	s=(n<<1)+1,t=(n<<1)+2;
	for(int i=1;i<=t;++i){
		h[i]=-1;
	}
	int u,v;
	for(int i=1;i<=n;++i){
		add(s,i,1);
		add(n+i,t,1);
	}
	for(int i=1;i<=m;++i){
		scanf("%d%d",&u,&v);
		add(u,n+v,1);
	}
	int ans=n;
	while(bfs()){
		ans-=dfs(s,INF);
	}
	for(int i=1;i<=n;++i){
		f[i]=i;
	}
	for(int i=1;i<=n;++i){
		f[i]=fnd(i);
	}
	for(int i=1;i<=n;++i){
		fa(i);
	}
	for(int i=1;i<=n;++i){
		if(f[i]==i){
			printf("%d ",i);
			prnt(i);
			puts("");
		}
	}
	printf("%d\n",ans); 
}

int main(){
	init();
	return 0;
}

lp2763 网络流24题-试题库问题

对于这一题,我们建三层的边来描述题目中的约束。
对于每道题建一个点,源点连一条容量为1的边到每一个点,表示每道题最多只能被选取一次。
对于每个种类建一个点,每一个点连一条容量为1的边到它所属的所有类,表示它仅可以被作为这个类来选取。
每个种类连一条容量为这个种类需要的选取次数的边到汇点,表示这个种类最多需要被选取的次数。
然后,跑一遍当前弧优化Dinic最大流,如果需要选取的总题数都能选取到,流量就等于需要选取的总题数。
当然也可以大力拆点跑二分图匹配,不过那样似乎更难写的样子。

#include<iostream>
#include<cstdio>
#include<queue>
const int INF=0x3f3f3f3f;

inline int Min(int A,int B){
	return A<B?A:B;
}

struct ee{
	int v;
	int w;
	int nxt;
}e[40005];
int h[2005],et=-1;
int dep[2005],nw[2005];

inline void Eadd(int U,int V,int W){
	e[++et]=(ee){V,W,h[U]};
	h[U]=et;
}

inline void add(int U,int V,int W){
	Eadd(U,V,W);
	Eadd(V,U,0); 
}

int n,m,s,t,Sans=0,tot; 

std::queue<int> q;
inline bool bfs(){
	for(int i=1;i<=t;++i){
		dep[i]=INF;
		nw[i]=h[i];
	}
	while(!q.empty()){
		q.pop();
	}
	dep[s]=0;
	q.push(s);
	int p;
	while(!q.empty()){
		p=q.front();
		q.pop();
		for(int i=h[p];i>=0;i=e[i].nxt){
			if(dep[e[i].v]==INF&&e[i].w){
				dep[e[i].v]=dep[p]+1;
				q.push(e[i].v);
			}
		}
	}
	return dep[t]<INF;
} 
//V表示从源点到当前路径上的剩余流量,cnt表示这个点向后流了多少流量。 
inline int dfs(int X,int V){
	if(V<=0||X==t){
		return V;
	}
	int cnt=0,val;
	for(int i=nw[X];i>=0;i=e[i].nxt){
		nw[X]=i;
		if(dep[e[i].v]==dep[X]+1){
			val=dfs(e[i].v,Min(V,e[i].w));
			if(val){
				cnt+=val;
				e[i].w-=val;
				e[i^1].w+=val;
				V-=val;
				if(V<=0){
					break;
				}
			}
		}
	}
	return cnt;
}

void init(){
	scanf("%d%d",&m,&n);
	s=m+n+1,t=m+n+2;
	for(int i=1;i<=t;++i){
		h[i]=-1;
	}
//	注意初始化。 
	int p,x;
	for(int i=1;i<=m;++i){
		scanf("%d",&x);
		Sans+=x;
		add(n+i,t,x);
	}
	for(int i=1;i<=n;++i){
		scanf("%d",&p);
		add(s,i,1);
		for(int j=1;j<=p;++j){
			scanf("%d",&x);
			add(i,n+x,1);
//			注意哈希。 
		}
	}
	int ans=0;
	while(bfs()){
		ans+=dfs(s,INF);
		printf("%d\n",ans); 
	}
	if(ans<Sans){
		puts("No solution!");
		return;
	}
	for(int i=1;i<=m;++i){
		printf("%d: ",i);
		for(int j=h[n+i];j;j=e[j].nxt){
			if((j&1)&&(e[j].w==1)&&(e[j].v!=t)){
				printf("%d ",e[j].v);
			}
		}
		puts("");
	}
}

int main(){
	init();
	return 0;
}

CF1091 Good Bye 2018

不知不觉就到了2018的最后一天。这个博客也有两个多月了。
我的姿势水平固然有了一定长进,但和巨神的距离却是越拉越远了。
这场CF也是,手速场我却没能有足够的手速。尤其是D题那种超简单的题目却死磕了半天才磕出来,F也没做出来。
总之,祝大家新年快乐_(:з」∠)_


CF1091A
依题意即可。
千万别掉进分类讨论的坑里。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
int a,b,c;

void init(){
	scanf("%d%d%d",&a,&b,&c);
	--b,c-=2;
	printf("%d",min(min(a,b),c)*3+3);
}
int main(){
	init();
	return 0;
}

CF1091B
我们将每个宝藏都加上每一个向量箭头,并标记目的地。
答案是任意一个被标记n次的。
可以用map或者hashmap维护。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
#include<map>
using namespace std;
int a[1005],b[1005],c[1005],d[1005];
typedef pair<int,int> pii;
typedef map<pii,int> mppii;
mppii mp;
int n;
void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d%d",a+i,b+i);
	}
	for(int i=1;i<=n;++i){
		scanf("%d%d",c+i,d+i);
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			pii nw(a[i]+c[j],b[i]+d[j]);
			++mp[nw];
		}
	}
	mppii::iterator it=mp.begin();
	while(it!=mp.end()){
		if(it->second==n){
			printf("%d %d\n",it->first.first,it->first.second);
			break;
		}
		++it;
	} 
}
int main(){
	init();
	return 0;
}

CF1091C
观察题意以后我们发现,对于\(k\),当且仅当\(n\%k == 0\)的时候,\(k\)会和其他的本质不同。
依据剩余系的性质容易证明。
那么我们就得到了一个朴素的求答案公式:
$$\forall x,st:n\% x==0,ans_{x}=\sum_{i=0}^{n/gcd(n,x)-1}(1+(ix-1)\%n+1) $$
然后考虑到\(n\%x==0\),所以
$$ans_{x}=\sum_{i=0}^{n/gcd(n,x)-1}(1+(ix-1)\%n)==\sum_{i=0}^{n/gcd(n,x)-1}(1+ix)$$
套上等差数列求和公式即可。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
inline long long gcd(long long A,long long B){
	return B?gcd(B,A%B):A;
}
long long n;
long long ans[100005],tp=0;

inline void calc(int X){
	long long tms=n/gcd(n,X);
	ans[++tp]=tms+((((tms*(tms-1))>>1)*X));
}

void init(){
	scanf("%I64d",&n);
	for(int i=1;i*i<=n;++i){
		if(!(n%i)){
			calc(i);
			if(i*i!=n){
				calc(n/i);
			}
		}
	}
	sort(ans+1,ans+1+tp);
	tp=unique(ans+1,ans+1+tp)-1-ans;
	for(int i=1;i<=tp;++i){
		printf("%I64d ",ans[i]);
	}
}
int main(){
	init();
	return 0;
}

CF1091D
由于长度为\(n\),显然序列的值必然是\(\frac{(n*(n+1))}{2}-SUM_i+SUM_{i+1}\), 其中\(SUM_{i}\)表示第\(i\)个排列的某个前缀和。
又排列的性质我们会发现,对于任意两个相邻的全排列,它的任意长度前缀和要么增加且前缀序列变化,要么前缀和保持不变且前缀序列保持不变。
故而,如果一个序列要满足要求,它只有两种情况——它本身就是一个排列,或者它横跨的两个排列必然有一部分前缀相同。
对于第一种情况我们可以看成它们的前缀有\(0\)个相同的。
问题转化为了,求所有排列中,两种相邻排列前缀相同的长度的和。
通过打表找规律,或者对全排列性质的精确推演,我们得到了这样一个求和式:
$$ ans=\sum_{pre=0}^{n-2}(n!-(\Pi_{i=n}^{n-pre+1}i))$$
但是暴力计算的复杂度最坏是\(n^2\)的,仍然无法通过此题。
我们考虑优化这个求和。优化的点在于\(\Pi_{i=n}^{n-pre+1}i\),我们(毫不)惊讶地发现,它等价于\(\frac{n!}{n-pre}\),所以我们可以预处理阶乘和阶乘的逆元。
这样就做完了。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std;
const long long MOD=998244353;

long long fac[1000005],inv[1000005];
long long n;

void init(){
	scanf("%I64d",&n);
	fac[0]=fac[1]=inv[0]=inv[1]=1;
	for(int i=2;i<=n;++i){
		fac[i]=fac[i-1]*i%MOD;
		inv[i]=(MOD-MOD/i)*inv[MOD%i]%MOD;
	}
	for(int i=2;i<=n;++i){
		inv[i]=inv[i-1]*inv[i]%MOD;
	}
	long long ans=fac[n];
	for(int i=1;i<=n-2;++i){
		ans+=(fac[n]-fac[n]*inv[n-i]%MOD+MOD)%MOD;
		ans%=MOD;
	}
	printf("%I64d",ans);
}
int main(){
	init();
	return 0;
}

lp1640 SCOI2010 连续攻击游戏

一般地,对于兼具可行性和唯一性的问题,我们考虑二分图匹配。
对于这一题来说,我们考虑拆点。
值得一提的是,这里的拆点并不是将一个点的两个属性拆开来,而是将一个点的属性和编号拆开来。然后跑匈牙利。
如果可以匹配,那就继续匹配;如果不能匹配,那就说明这个属性无论如何也取不到;或者取到它需要付出「前面有某个属性无法取到」的代价,那么就是没有答案了。

#include<iostream>
#include<cstdio>
struct ee{
	int v;
	int nxt;
}e[2000005];
int h[10005],et=0;
inline void add(int U,int V){
	e[++et]=(ee){V,h[U]};
	h[U]=et;
}

int n;
int vis[1000005],dep;
int usd[1000005];

inline bool dfs(int X){
	for(int i=h[X];i;i=e[i].nxt){
		int v=e[i].v;
		if(vis[v]!=dep){
			vis[v]=dep;
			if(!usd[v]||dfs(usd[v])){
				usd[v]=X;
				return 1;
			}
		}
	}
	return 0;
}

void init(){
	scanf("%d",&n);
	int a,b;
	for(int i=1;i<=n;++i){
		scanf("%d%d",&a,&b);
		add(a,i),add(b,i);
	}
	int ans=0;
	dep=0;
	for(int i=1;i<=n;++i){
		++dep;
		if(dfs(i)){
			++ans;
		}else{
			break;
		}
	}
	printf("%d\n",ans);
}

int main(){
	init();
	return 0;
}

lp1129 ZJOI2007 矩阵游戏

提示:行列匹配。


当知道这一题是行列匹配之后,做法就非常显然了。
我们观察发现,如果将每个行与每个列标上序号的话,题目的要求就是求两个排列,使得黑格子排在主对角线上。
然后我们考虑每一个黑格子,我们发现,在最终情况下的每一个黑格子,它所在的行的序号和列的序号均与开始时的序号相同。
故而,所求的两个排列中,相同位置的行和列在开始时的交点必然是黑色的。
所以,每个黑格子就在行列之间连一条边,用行来匹配列,如果都能一一匹配则说明有解。

#include<iostream>
#include<cstdio>
int mp[205][205];
int n,usd[205],vis[205],dep;
inline bool dfs(int X){
	for(int i=1;i<=n;++i){
		if(!mp[X][i]){
			continue;
		}
		if(vis[i]!=dep){
			vis[i]=dep;
			if(!usd[i]||dfs(usd[i])){
				usd[i]=X;
				return 1;
			}
		}
	}
	return 0;
} 
void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		usd[i]=0,vis[i]=0;
		for(int j=1;j<=n;++j){
			scanf("%d",&mp[i][j]);
		}
	}
	dep=0;
	for(int i=1;i<=n;++i){
		++dep;
		if(!dfs(i)){
			puts("No");
			return;
		}
	}
	puts("Yes");
}

int main(){
	int T;
	scanf("%d",&T);
	while(T--){
		init();
	}
	return 0;
}

lp5030 长脖子鹿放置

观察这一题的攻击方式,我们可以发现一个特点——所有奇数行的长颈鹿都攻击不到偶数行的长颈鹿,反之亦然。
所以,我们对奇偶的长颈鹿分开考虑,这显然是一张二分图。
于是类同于骑士共存问题,我们建一个二分图,跑匈牙利。
对于\(200\)的数据范围,匈牙利的小常数是可以卡过去的。
但是T了两个点,获得了80分的好成绩。
然后我们考虑一个对于vis数组的常数优化。
对于vis数组,我们定义一个变量dep,表示当前是第dep次匹配。这样就不必每次都清空vis数组了。
第二个就是,由于连的边是确定的,故而我们可以在线计算目的边,这样可以减少很多寻址时间。
另外就是对图的遍历顺序的优化。用了出题人的松松松顺序之后最终通过此题。

#include<iostream>
#include<cstdio>

const int dx[8]={3,3,-3,-3,1,1,-1,-1};
const int dy[8]={-1,1,-1,1,3,-3,3,-3};

bool mp[205][205];
int vis[40405],dep=0;
int n,m,q,usd[40405];
//注意数组大小。 

inline int calc(int X,int Y){
	return (X<=0||Y<=0||X>n||Y>m||mp[X][Y])?0:X*m+Y;
} 

inline bool dfs(int X){
	int v;
	for(int i=0;i<8;++i){
		v=calc((X-1)/m+dx[i],(X-1)%m+1+dy[i]);
//		注意还原信息。 
		if(v&&vis[v]!=dep){
			vis[v]=dep;
			if(!usd[v]||dfs(usd[v])){
				usd[v]=X;
				return 1;
			}
		}
	}
	return 0;
}

void init(){
//	mp[n][m]->mp[x][y] 
	scanf("%d%d%d",&n,&m,&q);
	int x,y;
	for(int i=1;i<=q;++i){
		scanf("%d%d",&x,&y);
		mp[x][y]=1;
	}
	int ans=n*m-q;
	for(int i=1;i<=n;i+=2){
		for(int j=1;j<=m;++j){
			if(mp[i][j]){
				continue;
			}
			++dep;
			ans-=dfs(calc(i,j));
		}
	}
//	for(int i=0;i<=n+1;++i){
//		for(int j=0;j<=m+1;++j){
//			printf("%2d ",calc(i,j));
//		}
//		puts("");
//	}
	printf("%d\n",ans);
}

int main(){
	init();
	return 0;
}

lp3355 网络流24题-骑士共存问题

首先我们观察这道题。
暴力地状压DP或者之类的操作显然会MLE/TLE。
我们发现,横坐标和纵坐标相加的和的奇偶性相同的格子永远不可能互相攻击到。
故而我们考虑建一张二分图。将横纵坐标的和的奇偶性不同的格子分别放在左右边,然后在能相互攻击到的点之间连边。依据题意,不能相互攻击到,所以答案即是这张图的最大独立集。

二分图最大独立集应当如何求得?我们有一个定理:一张二分图的最大独立集的大小等同于顶点数它的最大匹配的数量。
我们考虑感性地证明这个定理。
很容易可以发现,两个点不能被同时选取,当且仅当它们之间有连边。
又因为,有连边的两个点必然是属于二分图的两端的。
故而,如果有两个点不能被同时选取,它们必然可以构成一个匹配。
对于二分图上的最大匹配,所有没有被匹配的点都可以被选取。这非常的显然,因为没有被匹配到的点之间必然没有连边。如果有连边,那么就可以增加匹配,与最大匹配矛盾。
而,对于每一个匹配,都可以选取一组匹配中的一个点。这是因为,对于一些连通的匹配,它们有且仅有一个部分(左边或者右边)会和未匹配点连接。
这也是很显然的,因为如果不是这样的话,就存在一条新的增广路径,就可以得到更多的匹配了。
注意:坐标如何哈希成一个数字。

#include<iostream>
#include<cstdio>

const int dx[8]={-2,-1,1,2,-2,-1,1,2};
const int dy[8]={1,2,2,1,-1,-2,-2,-1}; 

struct ee{
	int v;
	int nxt;
}e[160005];
int h[40005],et=0;

inline void add(int U,int V){
	e[++et]=(ee){V,h[U]};
	h[U]=et; 
}

bool vis[40005],mp[205][205];
int n=0,m=0,usd[40005];

inline int calc(int X,int Y){
	return (X-1)*n+Y;
//	注意坐标哈希成数字的方式。 
}

inline bool dfs(int X){
	for(int i=h[X];i;i=e[i].nxt){
		if(!vis[e[i].v]){
			vis[e[i].v]=1;
			if(!usd[e[i].v]||dfs(usd[e[i].v])){
				usd[e[i].v]=X;
				return 1;
			}
		}
	}
	return 0;
}
int st[40005],tp=0;
char out[205][205];
void init(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
			mp[i][j]=0;
			usd[calc(i,j)]=0;
			out[i][j]='O';
		}
	}
	int x,y;
	for(int i=1;i<=m;++i){
		scanf("%d%d",&x,&y);
		mp[x][y]=1;
	}
	for(int i=1;i<=n;++i){
		for(int j=1;j<=n;++j){
//			printf("[%d %d] ",i,j);
			if(mp[i][j]||((i+j)&1)){
//				注意不是每次跳2而是判奇偶性。 
				continue;
			}
			st[++tp]=calc(i,j);
			out[i][j]='@'; 
			//注意有障碍的不仅是结束点,还有出发点。 
			for(int k=0;k<8;++k){
				x=i+dx[k],y=j+dy[k];
				if(x<=0||y<=0||x>n||y>n||mp[x][y]){
					continue;
				}
				out[x][y]='X';
				add(calc(i,j),calc(x,y));
			}
		}
	}
//	for(int i=1;i<=n;++i){
//		for(int j=1;j<=n;++j){
//			putchar(out[i][j]);
//		}
//		puts("");
//	}
	int ans=n*n-m;
//	减去被挖掉的点 
	for(int i=1;i<=tp;++i){
		for(int j=1;j<=n*n;++j){
			vis[j]=0;
		}
		ans-=dfs(st[i]);
	}
	printf("%d\n",ans);
}

int main(){
	init();
	return 0;
} 

lp5145 漂浮的鸭子

一道tarjan缩点的裸题。
我们仍然可以考虑一个很有趣的做法。那就是,把非环边删去,然后统计环的边权。

#include<iostream>
#include<cstdio>

struct ee{
	int v;
	int w;
}e[100005];

int n,in[100005];
bool vis[100005];

void init(){
	scanf("%d",&n);
	int v,w;
	for(int i=1;i<=n;++i){
		scanf("%d%d",&v,&w);
		e[i]=(ee){v,w};
		++in[v];
	}
	int ans=0,x,nw;
	for(int i=1;i<=n;++i){
		x=i;
		while(!in[x]&&!vis[x]){
			vis[x]=1;
			--in[e[x].v];
			x=e[x].v;
		}
	}
	for(int i=1;i<=n;++i){
		if(!vis[i]){
			nw=e[i].w;
			x=e[i].v;
			vis[i]=1;
			while(x!=i){
				vis[x]=1;
				nw+=e[x].w;
				x=e[x].v;
			}
			ans=std::max(ans,nw);
		}
	}
	printf("%d\n",ans);
}

int main(){
	init();
	return 0;
}

lp2756 网络流24题-飞行员配对方案问题

这是一个裸的配对问题。
我们可以建成一个二分图然后跑匈牙利。

#include<iostream>
#include<cstdio>
bool mp[105][105],vis[105];
int n,m,usd[105];

inline int dfs(int X){
    for(int i=1;i<=m;++i){
        if(!mp[X][i]){
            continue;
        }
        if(!vis[i]){
            vis[i]=1;
            if(!usd[i]||dfs(usd[i])){
                usd[i]=X;
                return 1;
            }
        }
    }
    return 0;
}

void init(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            mp[i][j]=mp[j][i]=0;
        }
    }
    int x=1,y=1;
    //你绝对想不到的错误——x,y没有初始化,导致判断它们的初始值的时候出现错误。 
    while(x>=0&&y>=0){
        scanf("%d%d",&x,&y);
        y-=n;
        mp[x][y]=1;
    }
    int ans=0;
    for(int i=1;i<=n;++i){
        for(int j=1;j<=m;++j){
            vis[j]=0;
        }
//        for(int j=1;j<=m;++j){
//        	printf("%d ",usd[j]);
//		}
//		puts("");
        ans+=(int)dfs(i);
    }
    printf("%d\n",ans);
    for(int i=1;i<=m;++i){
        if(usd[i]){
            printf("%d %d\n",usd[i],i+n);
            //注意这里要将编号还原。并且别输出反了。 
        }
    } 
}

int main(){
    init();
    return 0;
}

lp3376 【模板】网络最大流

全机房就我一个还不会网络流了。
下面我整理一下网络流的基本概念,辅助自己理解。


首先关于网络流的定义。形式化地说,网络流是一张图\(G(V,E)\)(有时也指这张图的一个状态),满足以下性质:
1.流量上限:对于任意一条边\(e\in E\),存在一个属性\(c_{e}\),定义它为这条边的流量上限。
2.流量受限:对于任意一条边\(e\in E\),存在一个属性\(0\le f_{e}\le c_{e}\),定义它为这条边当前的流量。
3.流量守恒:对于任意一个点\(v\),满足:\(\sum_{i\in{a|a_{v}=v,a\in E}}f_{i}=\sum_{j\in{b|b_{u}=v,b\in E}}f_{j}\)
4.源点汇点:存在两个点,分别叫做源点和汇点。其中,源点能够流出无穷多流量,而汇点能够流入无穷多流量。
若一个网络流的某一个状态满足上述所有性质,我们就称这个状态是合法的。
我们定义,对于一张网络流,使得它从源点流向汇点的流量最大的状态,为最大流。
求解最大流,就是指,求解最大流的状态。
为了辅助求解这个问题,我们可以考虑对于每一条边\(e\)都建一条反边\(e’\)。它的方向与原边相反。且\(f_{e’}=f_{e}\)
那么,所有的反边构成了另一张图\(G(E’,V)\),它描述的是它的原边已经走过的流量。
将一张图上的每一条边都只保留它的当前剩余流量\(f_{e}\),这张图可以被认为是对原图的流量空余的描述。故而我们称这张图为残余网络。

我们定义增广路径,指的是,在包括反边的残余网络的一条路径上面的一条路径,使得路径上的每一条原边都是不满的,且路径上每一条反边都是非空的。
对于关于这条路径上的所有原边,将其流量上限减去当前流量的值减去最小值,再对路径上每一条反边的当前流量值取最小值,再对两个最小值取最小值。
然后将所有原边的流量加上这个最小值,并将每一条反边的流量减去这个最小值(对应的反/原边同时减/加相应值)。 我们称这样一个操作为增广,称操作中减少反边流量的子操作为撤销。
如果当前图不是处于最大流的状态,那么一定可以找到一个增广路;如果当前图属于最大流的状态,那么一定没有增广路。
我们朴素地考虑如何利用增广来取得最大路径。如果我们每一次都暴力地寻找整个网络上的一条可行增广路并且增广(这似乎被称作F-F算法),在很糟糕的情况下,可能会出现很多次反复地增广而只增加很少的流量的情况。
在上诉的算法中,可能存在一种情况,就是,一条边被反复地增广-撤销多次。这样无形中增大了很多复杂度。
我们考虑上诉的情况是如何产生的。很显然,经过一次撤销操作之后,当前的路径的长度减少了一条边,而原来经过那条边的那些流量所在的路径上的长度也减少了一条边。
那么反复地增广-撤销操作的一个很大的原因,便是,第一次增广到这条边的时候的路径经过的边数太多。
如果每一次搜索增广路径的时候,都尽可能搜索边数尽可能少的一条边,那么便可以使得增广-撤销操作尽可能少。(这似乎被称作E-K算法)
上述的算法都可以被称作是朴素的算法。但复杂度最坏情况下可能会被卡到非常大,大概是\(O(n\times m^2)\)级。


下面是一个对于这个算法的正确性的感性证明。
我们首先证明,每一次增广之后,图上流过的流量都增多。
对于一次增广,当我们流过一条边的时候,流过的流量存在两种情况:
1.这些流量走过的是反边。
2.这些流量走过的是原边。
对于流过原边的流量,很显然无论如何可以流的,并且会将原图的状况改进得更优。
而对于流过反边的流量,可以视为将这些流量还原,然后原来流到这条边的那些流量,现在从当前路径流过这条边之后的那部分流走;而当前流到这条边的流量,则从之前流过这条边的那些流量的后半部分流走。
所以,每一次增广,总是会导向一个合法状态。
又因为,每一次增广之后,源点都会流出更多的流量,汇点都会收到更多的流量,所以整张图流过的流量增加了。

然后我们考虑证明,当不再能增广的时候,图上处于最大流的状态。
当走过一条边的时候,我们事实上完成了一次「反悔」,也就是指,让之前从这里走的一些流量往另一个方向走了。
如果,不再可以增广,这就意味着,无论反悔到什么程度、无论怎么改变之前已有的流量的路径,都无法找到一条新的路径使得流量可以通过。
即,哪怕是在反悔最多的情况下,也找不到一条能够产生更多流量的路径。
那么,我们就可以断言这个状态已经是最大流的状态了。


回到算法本身上。下面我们考虑优化这个朴素算法。\(O(n\times m^2)\)的复杂度显然是不可接受的。
Dinic算法就是一种对上述朴素算法的优化。
我们考虑一种被称为「多路增广」的操作。这种操作也是Dinic的核心。
对于每一次多路增广,我们首先将整张图dfs一遍,预处理出深度小于源汇距离的所有节点的深度。这样我们得到了一张分层图。
然后我们从源点开始进行一种特殊的DFS——每一次访问的节点的深度必须比前一个节点深,并且搜索经过的边必须是可以增广的边。
当我们搜索到汇点的时候,便开始沿着来路回溯,直到回溯到第一个「来路仍然可以继续增广」并且「连着其他还可以被继续增广的边」的节点为止,然后继续向下搜索。
如果回溯到了源点,多路增广结束。
然后,对剩下可以走的部分,重新计算深度,并重跑增广,直到不能再增广为止。
对于每一次多路增广,我们可以以\(O(nm)\)的复杂度遍历完成对某一个特定深度的所有增广操作。 而多路增广显然是最多只会被执行\(n\)次的。故而这么做的复杂度是\(O(n^2+m)\)的。
我们就得到了一个相对较优的算法。


#include<iostream>
#include<cstdio>
#include<queue>

const int INF = 0x3f3f3f3f;

inline int Min(int A,int B){
	return A<B?A:B;
}

struct ee{
	int v;
	int w;
	int nxt;
}e[200005];
int h[10005],et=-1;
int n,m,s,t,ans=0,dep[10005],nw[10005];
inline void add(int U,int V,int W){
	e[++et]=(ee){V,W,h[U]};
	h[U]=et;
}
std::queue<int> q;
inline bool bfs(){
	for(int i=1;i<=n;++i){
		dep[i]=INF;
		nw[i]=h[i];
	}
	while(!q.empty()){
		q.pop();
	}
	dep[s]=0;
	q.push(s);
	int p;
	while(!q.empty()){
		p=q.front();
		q.pop();
		for(int i=h[p];i>=0;i=e[i].nxt){
			if(dep[e[i].v]==INF&&e[i].w){
				dep[e[i].v]=dep[p]+1;
				q.push(e[i].v);
			}
		}
	}
	return dep[t]<INF;
}

inline int dfs(int X,int V){
	if((V<=0)||(X==t)){
		return V;
	}
	int cnt=0,val;
	for(int i=nw[X];i>=0;i=e[i].nxt){
		nw[X]=i;
		if(dep[e[i].v]==dep[X]+1){
			val=dfs(e[i].v,Min(V,e[i].w));
			if(val){
				cnt+=val;
				V-=val;
				e[i].w-=val;
				e[i^1].w+=val;
				if(V<=0){
					break;
				}
			}
		}
	}
	return cnt; 
}

void init(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	int u,v,w;
	for(int i=1;i<=n;++i){
		h[i]=-1;
	}
	for(int i=1;i<=m;++i){
		scanf("%d%d%d",&u,&v,&w);
		add(u,v,w);
		add(v,u,0);
	}
	while(bfs()){
		ans+=dfs(s,INF);
		//注意应当填写起始点。 
	}
	printf("%d\n",ans);
}

int main(){
	init();
	return 0; 
}

lp4117 Ynoi2018 五彩斑斓的世界

虽然我没有玩过这个Galgame,但是就截图而言画风还是挺可爱的。
抱歉离题了。
这是一道由乃省的省选题。
大家都知道喜欢由乃的某人最喜欢的就是数据结构了。
仔细观察这道题的数据范围,我们可以想到分块。
对于每个块里面数值相同的数维护一个并查集,对于每一个并查集维护它的大小。那么我们每一次修改,如果是暴力修改就只需要改变一个元素所在的集合,如果是整块修改就将并查集合并。
查询的时候块外部分暴力更新,快内部分按块查询即可。
但是这一题的常数简直丧心病狂,毒瘤lxl简直是魔鬼。SIZE大了T,SIZE小了MLE。我卡了整整十九次,最终结合前人优秀经验,使用unsigned char成功卡了过去。
大家记住这组魔法的数字:253-410

#include<iostream>
#include<cstdio>
#include<cmath>
#define MAXNUM 410
int SIZE,NUM;
int bl[100005],lb[MAXNUM],rb[MAXNUM];
int a[100005],tgb[MAXNUM];
int val[100005],mx[100005],fa[100005];
unsigned char cnt[MAXNUM][100005],loc[MAXNUM][100005];
int n,m;
inline int fthr(int X){
	while(X^fa[X]){
		X=fa[X]=fa[fa[X]];
	}
	return X;
}
inline void uni(int X,int A,int B){
	//将X块中的A合并到B处。若B处存在则直接合并,否则将B移动到A并修改A的值。 
	loc[X][B]?fa[loc[X][A]+lb[X]-1]=loc[X][B]+lb[X]-1:(loc[X][B]=loc[X][A],val[loc[X][A]+lb[X]-1]=B);
	cnt[X][B]+=cnt[X][A],cnt[X][A]=loc[X][A]=0;
}
inline void del(int S,int X){
	//如果X比最大值的一半还要小,那么我们考虑将所有小于X的上移,并且修改标记;否则将所有大于X的下移。 
	if((X<<1)<=mx[S]-tgb[S]){
		for(int i=tgb[S]+1;i<=tgb[S]+X;++i){
			if(loc[S][i]){
				uni(S,i,i+X);
			}
		}
		tgb[S]+=X; 
	}else{
		for(int i=X+tgb[S]+1;i<=mx[S];++i){
			if(loc[S][i]){
				uni(S,i,i-X);
			}
		}
		mx[S]=std::min(mx[S],tgb[S]+X);
	}
}
inline void pshd(int X){
	for(int i=lb[X];i<=rb[X];++i){
		a[i]=val[fthr(i)];
		loc[X][a[i]]=cnt[X][a[i]]=0;
		//应当先清空再修改标记。 
		a[i]-=tgb[X];
	}
	for(int i=lb[X];i<=rb[X];++i){
		fa[i]=0;
	}
	tgb[X]=0;
}
inline void updt(int X){
	mx[X]=0;
	for(int i=lb[X];i<=rb[X];++i){
		mx[X]=std::max(mx[X],a[i]);
		loc[X][a[i]]?(fa[i]=loc[X][a[i]]+lb[X]-1):(fa[i]=i,val[i]=a[i],loc[X][a[i]]=i-lb[X]+1);
		++cnt[X][a[i]];
	}
}
inline void chng(int L,int R,int X){
	if(bl[L]^bl[R]){
		pshd(bl[L]),pshd(bl[R]);
		for(int i=L;i<=rb[bl[L]];++i){
			if(a[i]>X){
				a[i]-=X;
			}
		}
		for(int i=lb[bl[R]];i<=R;++i){
			if(a[i]>X){
				a[i]-=X;
			}
		}
		updt(bl[L]),updt(bl[R]);
		for(int i=bl[L]+1;i<=bl[R]-1;++i){
			del(i,X);
		}
	}else{
		pshd(bl[L]);
		for(int i=L;i<=R;++i){
			if(a[i]>X){
				a[i]-=X;
			}
		}
		updt(bl[L]);
	}
}
inline int qry(int L,int R,int X){
	if(bl[L]^bl[R]){
		int rt=0;
		for(int i=L;i<=rb[bl[L]];++i){
			if(val[fthr(i)]-tgb[bl[L]]==X){
				++rt;
			}
		}
		for(int i=lb[bl[R]];i<=R;++i){
			if(val[fthr(i)]-tgb[bl[R]]==X){
				++rt;
			}
		}
		//注意这里减去的是其所在块的lzytg 
		for(int i=bl[L]+1;i<=bl[R]-1;++i){
			rt+=((X+tgb[i]<=100000)?cnt[i][X+tgb[i]]:0);
		}
		return rt;
	}else{
		int rt=0;
		for(int i=L;i<=R;++i){
			if(val[fthr(i)]-tgb[bl[L]]==X){
				++rt;
			}
		}
		return rt;
	}
}
inline void DEBUG(){
	for(int i=1;i<=n;++i){
		printf("%d/%d ",val[fthr(i)],a[i]);
	}
	puts("");
	for(int i=1;i<=NUM;++i){
		printf("[%d %d|%d|%d],",lb[i],rb[i],tgb[i],mx[i]);
	}
	puts("");
	/*
	for(int i=1;i<=100;++i){
		if(loc[2][i]){
			printf("{%d} ",i);
		}
	} 
	*/
}
void init(){
	scanf("%d%d",&n,&m);
	SIZE=253;
	NUM=std::ceil((double)n/SIZE);
	for(int i=1;i<=NUM;++i){
		lb[i]=(i-1)*SIZE+1;
		rb[i]=i*SIZE;
		for(int j=lb[i];j<=rb[i];++j){
			bl[j]=i;
		}
	}
	rb[NUM]=n;
	for(int i=1;i<=n;++i){
		scanf("%d",a+i);
	}
	for(int i=1;i<=NUM;++i){
		updt(i);
	}
	int op,l,r,x;
	for(int i=1;i<=m;++i){
//		DEBUG();
		scanf("%d%d%d%d",&op,&l,&r,&x);
		if(op==1){
			chng(l,r,x);
		}else{
			printf("%d\n",qry(l,r,x));
		}
	}
}

int main(){
//	freopen("test.out","w",stdout);
	init();
	return 0;
}

AT1219 历史研究

一道回滚莫队的例题。
莫队算法能够解决很多类似于求和的询问问题。但是对于求最值的询问它无能为力。这是因为莫队需要一个区间既能够拓展又能够收缩。
对于求最值类的问题,它只能够拓展,不能够收缩。
这时候我们可以使用一种被称为「回滚莫队」的算法。
具体来说,对于每个块内的询问,我们都分成两部分来处理。一部分是将右端点推进,另一部分则是将左端点推进。对于每一个询问,我们在求得它向右端点推进的值以后,暴力统计上左端点对它的贡献即可。
请注意回滚莫队不能奇偶分块。结合回滚莫队的性质可以很容易理解。

#include<iostream>
#include<cstdio>
#include<cmath>
#include<algorithm>
int BLOCK,NUM;
int bl[100005],lb[100005],rb[100005];
struct Query{
	int l;
	int r;
	int id;
	inline bool operator<(const Query &B)const{
		return (bl[l]^bl[B.l])?(bl[l]<bl[B.l]):(r<B.r);
	}
}q[100005];
int n,m,a[100005],cnt1[100005],cnt2[100005],loc[100005],typ[100005];
long long ans[100005];
void init(){
	scanf("%d%d",&n,&m);
	BLOCK=std::sqrt(n);
	NUM=std::ceil((double)n/BLOCK);
	for(int i=1;i<=NUM;++i){
		lb[i]=BLOCK*(i-1)+1;
		rb[i]=BLOCK*i;
		for(int j=lb[i];j<=rb[i];++j){
			bl[j]=i;
		}
	}
	rb[NUM]=n;
	for(int i=1;i<=n;++i){
		scanf("%d",a+i);
		loc[i]=a[i];
	}
	std::sort(loc+1,loc+1+n);
	int tot=std::unique(loc+1,loc+1+n)-loc-1;
	for(int i=1;i<=n;++i){
		typ[i]=std::lower_bound(loc+1,loc+1+tot,a[i])-loc;
	}
	for(int i=1;i<=m;++i){
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
	}
	std::sort(q+1,q+1+m);
	int p=1;
	for(int i=0;i<=NUM;++i){
		int l=rb[i]+1,r=rb[i];
		long long nw=0;
		for(int j=1;j<=n;++j){
			cnt1[j]=0;
		}
		for(;bl[q[p].l]==i;++p){
			int ql=q[p].l,qr=q[p].r;
			long long tmp=0;
			if(bl[ql]==bl[qr]){
				for(int j=ql;j<=qr;++j){
					cnt2[typ[j]]=0;
				}
				for(int j=ql;j<=qr;++j){
					++cnt2[typ[j]];
					tmp=std::max(tmp,1ll*cnt2[typ[j]]*a[j]);
				}
				ans[q[p].id]=tmp;
				continue;
			}
			while(r<qr){
				++r;
				++cnt1[typ[r]];
				nw=std::max(nw,1ll*cnt1[typ[r]]*a[r]);
			}
			tmp=nw;
			while(l>ql){
				--l;
				++cnt1[typ[l]];
				nw=std::max(nw,1ll*cnt1[typ[l]]*a[l]);
			}
			ans[q[p].id]=nw;
			while(l<rb[i]+1){
				--cnt1[typ[l]];
				++l;
			}
			nw=tmp;
		}
	}
	for(int i=1;i<=m;++i){
		printf("%lld\n",ans[i]);
	}
}

int main(){
	init();
	return 0;
}

CF600E Lomsat gelral

一道树上启发式合并的例题。
首先考虑暴力的做法。统计每一个子节点的信息,然后传到父节点。
我们发现这样做的复杂度是\(O(n^2)\)的。显然,复杂度是错误的。
我们考虑修改统计子节点的顺序。
重链剖分有一个很有意义的性质:一个点通向根的路径上的轻边的数量最多不会超过\(log_n\)。这利用重链剖分的性质可以轻松用反证法证明。
那么,依据这个性质,我们可以统计轻边、上传重边。这也就意味着,对于每一次访问,如果它是一条轻边,那么就把它的信息统计完了清空;如果是一条重边,那么把它的信息上传。
例如,我们现在有一个节点\(X\),那么我们先计算它的所有轻节点,然后我们再计算它的重节点,最后将它除了重节点以外的地方都搜一遍,这样就获得了它的所有信息。
为什么这样做复杂度是对的呢?这就可以基于刚刚那个性质来证明了。具体地来说,一个节点会被清理信息,当且仅当它是一个节点下面的一个轻节点。也就是说,它被清空的次数就是它通向根的路径上轻边的数量。
由刚才那个性质可得,这个值是对数级的,所以复杂度是对的。
注意sm数组的tp的控制。

#include<iostream>
#include<cstdio>
struct ee{
	int v;
	int nxt;
}e[200005];
int h[100005],et=0;
inline void add(int U,int V){
	e[++et]=(ee){V,h[U]};
	h[U]=et;
}
int sn[100005],sz[100005],a[100005],cnt[100005];
bool kp[100005];
long long ans[100005];//ans表示每个节点的答案 
long long sm[100005];//sn表示出现次数为cnt次的颜色值的和 
int n;
inline void dfs1(int X,int FA){
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v!=FA){
			dfs1(e[i].v,X);
		}
	}
	sz[X]=1;
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v){
			sz[X]+=sz[e[i].v];
			if(sz[e[i].v]>sz[sn[X]]){
				sn[X]=e[i].v;
			}
		}
	}
}
int tp;
inline void rnw(int X,int FA,int V){
	sm[cnt[a[X]]]-=a[X];
	cnt[a[X]]+=V;
	sm[cnt[a[X]]]+=a[X];
	if(sm[tp+1]){
		++tp;
	}
	if(!sm[tp]){
		--tp;
	}
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v!=FA&&!kp[e[i].v]){
			rnw(e[i].v,X,V);
		}
	}
}
inline void dfs2(int X,int FA,int isSn){
	for(int i=h[X];i;i=e[i].nxt){
		if(e[i].v!=FA&&e[i].v!=sn[X]){
			dfs2(e[i].v,X,0);
		}
	}
	if(sn[X]){
		dfs2(sn[X],X,1);
		kp[sn[X]]=1;
	}
	rnw(X,FA,1);
	kp[sn[X]]=0;
	ans[X]=sm[tp];
	if(!isSn){
		rnw(X,FA,-1);
	} 
}
void init(){
	scanf("%d",&n);
	for(int i=1;i<=n;++i){
		scanf("%d",&a[i]);
	}
	int u,v;
	for(int i=1;i<n;++i){
		scanf("%d%d",&u,&v);
		add(u,v),add(v,u);
	}
	dfs1(1,0);
	dfs2(1,0,1);
	for(int i=1;i<=n;++i){
		printf("%lld ",ans[i]);
	}
}

int main(){
	init();
	return 0;
}

lp1494 国家集训队 小Z的袜子

令颜色为\(c_i\)为第\(i\)种颜色在给定区间内出现的次数。
则,题目所求为:
$$\frac{\sum_{i=1}^{n}c_{i}(c_{i}-1)}{(r-l)(r-l+1)}$$
展开可以得到:
$$\frac{\sum_{i=1}^{n}c_{i}^2-\sum_{i=1}^{n}c_{i}}{(r-l)(r-l+1)}$$
由性质可得:
$$\sum_{i=1}^{n}c_{i}=r-l+1$$
故而,我们需要求的仅有:
$$\sum_{i=1}^{n}c_{i}^2$$
对于这个式子的求法,我们很容易可以想到莫队。
我们考虑一个区间添加上一个数与删除一个数造成的影响。
令修改的颜色为\(x\),则:
$$\Delta v=\sum_{i=1}^{n}c_{i}^2-c_{x}^{2}+(c_{x}±1)^2-\sum_{i=1}^{n}c_{i}^2$$
展开可得:
$$\Delta v=1±2c_{x}$$

下面考虑莫队。
莫队的原理大致是,将原数列分块,然后将询问以左端点在原数列中的块为第一关键字,右端点为第二关键词来排序。
虽然说它是一种优化后的暴力,但是,若令一次修改的复杂度是\(f_{n}\),根据最小直线斯坦纳树的性质,可以证明这么做的复杂度是 \(O(f_{n} n \sqrt{m})\),可以通过此题。
依据毒瘤lxl的说法,莫队最快的分块大小是\(n/\sqrt{m*2/3}\)
故而我们对原数列分块完以后,将询问排序。然后开始移动区间。
扫一遍即可。
另外就是一个小优化,排序的时候分奇偶排序,也就是说,奇数块右端点递增,偶数块右端点递减。
原理很简单,就是移过去再移回来,路上的点都扫了一遍。

#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cmath>
int BLOCK;
int belong[50005];
struct Query{
	int l;
	int r;
	int id;
	int len;
	inline bool operator <(const Query &B)const{
		return (belong[l]^belong[B.l])?(belong[l]<belong[B.l]):((belong[l]&1)?r<B.r:r>B.r);
	}
}q[50005];
long long tot=0;
int cnt[50005];
inline void add(int X){
	tot+=(cnt[X]<<1|1);
	++cnt[X];
}
inline void del(int X){
	tot+=(1-(cnt[X]<<1));
	--cnt[X];
}
inline long long Gcd(long long A,long long B){
	return B?Gcd(B,A%B):A;
}
long long ans1[50005],ans2[50005];
int n,m,a[50005];
void init(){
	scanf("%d%d",&n,&m);
	BLOCK=n/std::sqrt(m*2.0/3.0); 
	for(int i=1;i<=n;++i){
		scanf("%d",a+i);
	}
	for(int i=1;i<=m;++i){
		scanf("%d%d",&q[i].l,&q[i].r);
		q[i].id=i;
		q[i].len=q[i].r-q[i].l+1;
		ans2[i]=1;
	}
	for(int i=1;i<=n;++i){
		belong[i]=(i-1)/BLOCK+1;
	}
	std::stable_sort(q+1,q+1+m);
	int l=q[1].l,r=q[1].l-1;
	long long nwgcd;
	for(int i=1;i<=m;++i){
		while(l<q[i].l){
			del(a[l]);
			++l;
		}
		while(l>q[i].l){
			--l;
			add(a[l]);
		}
		while(r>q[i].r){
			del(a[r]);
			--r;
		}
		while(r<q[i].r){
			++r;
			add(a[r]);
		}
		if(tot!=q[i].len){
			ans1[q[i].id]=tot-q[i].len,ans2[q[i].id]=1LL*(q[i].r-q[i].l)*q[i].len;
			nwgcd=Gcd(ans1[q[i].id],ans2[q[i].id]);
			ans1[q[i].id]/=nwgcd,ans2[q[i].id]/=nwgcd;
		}
	}
	for(int i=1;i<=m;++i){
		printf("%lld/%lld\n",ans1[i],ans2[i]);
	}
}

int main(){
	init();
	return 0;
}