常用技巧精选(一)

尺取法 Subsequence POJ No.3061 (1)

求连续子序列中总和不小于S的最小长度。
由于所有的元素都大于0,如果子序列[s,t)满足as+···+at-1>=S,那么对于任何的t<t’一定有as+···+at’-1>=S。此外对于区间[s,t)上的总和来说如果令
sum(i)=a0+a1+···+ai-1
那么
as+as+1+···+at-1=sum(t)-sum(s)
因此预先以O(n)的时间计算好sum的话,就可以以O(1)的时间计算区间上的总和。这样一来,子序列的起点s确定之后,便可以用二分搜索快速地确定使序列和不小于S的结尾t的最小值。

int a[10005],n,S;
void solve(){
	int x;
	for(int i=1;i<=n;i++){
		cin >> x;
		a[i]=a[i-1]+x;
	}
	if(a[n]<S){
		cout << 0 << endl;
		return;
	}
	int res=n;
	for(int s=0;a[s]+S<=a[n];s++){
		int t=lower_bound(a+s,a+n,a[s]+S)-a;//找始终比当前大S的子序列(即s到t这一段路程是超过S的) 
		res=min(res,t-s);
	}
	cout << res << endl; 
}
int main(){
	cin >> n >> S;
	solve();
	return 0;
}

复杂度为O(nlogn)
第二种方法
最小子序列是大于等于S的那么这个序列任意减掉一个元素它就是小于S的了。
1.将s=t=sum=0初始化
2.只要有sum<S,就不断将sum增加a,并将t增加1
3.如果2无法满足sum>=S则终止,否则跟新res=min(res,t-s)
4sum减去as,s增加1然后回到2

int a[10005],n,S;
void solve2(){
	for(int i=0;i<n;i++){
		cin >> a[i];
	}
	int res=n+1;
	int s=0,t=0,sum=0;
	while(true){
		while(t<n&&sum<S){
			sum+=a[t++];
		}
		if(sum<S){
			break;
		}
		res=min(res,t-s);
		sum-=a[s++];
	}
	if(res>n){
		cout << 0 << endl;
	}else{
		cout << res << endl;
	}
} 
int main(){
	cin >> n >> S;
	solve2();
	return 0;
}

像这样反复地推进区间地开头和末尾,来求取满足条件地最小区间地方法称为尺取法。

Jessica’s Reading Problem POJ No.3320 (2)

也可用尺取法

int P;
int a[1000005];
void solve(){
	set<int> all;
	for(int i=1;i<=P;i++){
		all.insert(a[i]);
	}
	int n=all.size();
	int s=1,t=1,num=0;
	int res=P;
	map<int,int> count;//知识点->使用次数
	for(;;){
		while(t<=P&&num<n){
			if(count[a[t]]==0){
				num++; 
			}
			count[a[t++]]++;
		}
		if(num<n)break;
		res=min(res,t-s);
		if(--count[a[s++]]==0){
			num--;
		}
	}
	cout << res << endl;
}
int main(){
	cin >> P;
	for(int i=1;i<=P;i++){
		cin >> a[i];
	}
	solve();
	return 0;
}

复杂度为O(PlogP)

反转(开关问题) Face The Right Way POJ No.3276 (1)

首先,交换区间反转地顺序对于结果是没有影响地。此外,可以知道对同一个区间进行两次以上地反转时多余的。由此,问题就转化为了求需要杯反转地区间的集合。于是我们先考虑一下最左端的牛。包含这头牛的区间只有一个,因此如果这头牛面朝前方,我们就能知道这个区间不需要反转。
反之,如果这头牛面朝后方,对应的区间就必须进行反转了。而且在此之后这个最左的区间就再也不需要考虑了。这样依赖,通过考虑最左边的牛,问题的规模就缩小了1,不断地重复下去,就可以无需搜索求出最少所需地反转次数了。
首先我们对所有的K都求解一次,对于每个K我们都要从最左端开始考虑N头牛地情况,此时最坏情况需要进行N-K+1次的反转操作,而每次操作又要反转K头牛,所以总的夫再度为O(N3)。但是区间反转部分还是很容易进行优化的。
记f[i]:=区间[i,i+K-1]进行了反转的话则为1,否则为0
这样,在考虑第i头牛的时,如果Σj=i-K+1i-1为奇数的话,则这头牛的方向是与起始方向相反的。

int N;
int dir[5005];
int f[5005];
int calc(int K){
	memset(f,0,sizeof(f));
	int res=0;
	int sum=0;//f的和
	for(int i=0;i+K<=N;i++){
		//计算区间[i,i+K-1]
		if((dir[i]+sum)%2!=0){
			//前i到i+k-1的值为奇数代表转换过,如果本来就是面对背后的再加上当前的奇数就变为偶数了
			res++;
			f[i]=1; 
		}
		sum+=f[i];
		if(i-K+1>=0){
			sum-=f[i-K+1];//减去上一个区间的值 
		}
	} 
	//检查剩下的牛是否有面朝后方的情况
	for(int i=N-K+1;i<N;i++){
		if((dir[i]+sum)%2!=0){
			//无解
			return -1; 
		}
		if(i-K+1>=0){
			sum-=f[i-K+1];
		}
	}
	return res; 
}
void solve(){
	int K=1,M=N;
	for(int k=1;k<=N;k++){
		int m=calc(k);
		if(m>=0&&M>m){
			M=m;
			K=k;
		}
	}
	cout << K << " " << M << endl;
}
int main(){
	char c;
	cin >> N; 
	for(int i=0;i<N;i++){
		cin >> c;
		if(c=='B'){
			dir[i]=1;
		}
	}
	solve();
	return 0;
}

Fliptile POJ No.3279

首先,同一个各自反转两次的话就会恢复原状,所以多次翻转式多余的。此外,翻转的格子的集合相同的话,其次序式无关紧要的。因此,总共有2NM种翻转方法。
可以先确定第一行的翻转方式,然后可以很容易判断这样是否存在解以及解的最小步数是多少,这样将第一行的所有翻转方式都尝试一次就能求出整个问题的最小步数。这个算法种最上面的一行的翻转方式共有2N种,复杂度为O(NM2N)

#include<iostream>
#include<cstdio>
#include<cstring>
using namespace std;
//输入 
int N,M;
int title[20][20];
//状态数组 
int dx[]={-1,0,0,0,1};
int dy[]={0,-1,0,1,0};
//保存最优解
int opt[20][20];
//保存中间结果
int flip[20][20];
//查询(x,y)的颜色 
int get(int x,int y){
	int c=title[x][y];//0代表白色 1代表黑色 
	for(int d=0;d<5;d++){
		int x2=x+dx[d];
		int y2=y+dy[d];
		if(0<=x2&&x2<M&&0<=y2&&y2<N){
			c+=flip[x2][y2];
		}
	}
	return c%2;//如果本来是白色  则翻转了奇数次之后就成了黑色,偶数次之后就是白色  如果是黑色  奇数次之后是白色  偶数次之后就是黑色 
}
//在第一行确定的情况下的最小操作次数 
int calc(){
	//求出第二行开始的翻转方法
	for(int i=1;i<M;i++){
		for(int j=0;j<N;j++){
			if(get(i-1,j)!=0){
				//如果这个格子是黑色的话那么就要翻转它 
				flip[i][j]=1;
			}
		}
	}
	//判断最后一行是否全白 
	for(int j=0;j<N;j++){
		if(get(M-1,j)!=0){
			return -1;
		}
	}
	int res=0;
	//统计翻转的次数
	for(int i=0;i<M;i++){
		for(int j=0;j<N;j++){
			res+=flip[i][j];
		}
	} 
	return res;
}
void solve(){
	int res=-1;
	//按照字典序尝试2^N次方种可能 
	for(int i = 0;i < 1 << N ; i++){
		memset(flip,0,sizeof(flip));
		for(int j=0;j<N;j++){
			flip[0][N-j-1]=i>>j&1;
		}
		int num=calc();
		if(num>=0&&(res<0||res>num)){
			res=num;
			memcpy(opt,flip,sizeof(flip));
		}
	}
	if(res<0){
		cout << "IMPOSSIBLE" << endl;
	}else{
		for(int i=0;i<M;i++){
			for(int j=0;j<N;j++){
				printf("%d%c",opt[i][j],j==N-1?'\n':' ');
			}
		}
	}
} 
int main(){
	cin >> M >> N;//M行N列 
	for(int i=0;i<M;i++){
		for(int j=0;j<N;j++){
			cin >> title[i][j];
		}
	}
	solve();
	return 0;
} 

重点

	for(int i = 0;i < 1 << N ; i++){
		memset(flip,0,sizeof(flip));
		for(int j=0;j<N;j++){
			flip[0][N-j-1]=i>>j&1;
		}
	}

这段代码用于按字典序排列每一个数。
假设N=4且M=4
输出为这样:

/*
0 0 0 0
0 0 0 1
0 0 1 0
0 0 1 1
0 1 0 0
0 1 0 1
0 1 1 0
0 1 1 1
1 0 0 0
1 0 0 1
1 0 1 0
1 0 1 1
1 1 0 0
1 1 0 1
1 1 1 0
1 1 1 1
*/

专栏 集合的整数表示

前面的代码里,为了尝试第一行的所有可能性,使用了集合的整数表现。在程序种表示集合的方法有很多,当元素数比较少时,像这样用二进制码比较方便。集合{0,1,···,n-1}的子集S可以用以下方式编码成整数。
f(S)=Σi∈S2i
像这样表示之后,一些集合运算可以对应地写成以下形式。

  • 空集:··································································0
  • 只含有第i个元素的集合{i}: ········································1 << i
  • 含有全部n个元素的集合{0,1,···,n-1}:·························(1>>n)-1
  • 判断第i个元素是否属于集合S:·································if (S>>i&1)
  • 向集合中加入第i个元素S∪{i}:·································S|1<<i
  • 从集合中取出第i个元素S{i}:···································S&~(1<<i)
  • 集合S和T的并集S∪T:··········································S|T
  • 集合S和T的交集S∩T:···········································S&T
    此外,如果想要将集合{0,1,···,n-1}的所有子集都枚举出来的话,可以像下面这样书写
for(int S=0;S < 1 >> n ;S++){
	//对子集的操作
}

按照这个顺序进行循环的话,S便会从空集开始,然后按照{0}、{1}、{0,1}、···、{0,1,···,n-1}的升序顺序枚举出来。
接下来介绍如何枚举某个集合sup的子集,这里sup是一个二进制码,其本身也是某个集合的子集。例如给定了01101101这样的集合,要将01100000或者00101101等子集枚举出来。前面是从0开始不断加1枚举出了全部的子集。此时,sup+1并不一定是sup的子集。而(sup+1)&sup虽然是sup的子集,可是很有可能依旧是sup,没有任何改变
所以我们要反过来,从sup开始每次减1直到0位置,由于sup-1并不一定是sup的子集,所以我们把它与sub进行按位与&。这样的话就可以将sup所有的子集按照降序列举出来。(sup-1)&sup会忽略sup中的0而从sup中减1.

int sub=sup;
do{
	//对自己的操作
	sub=(sub-1)&sup
}while(sub!=sup);//处理完0之后,会有-1&sup=sup

最后我们介绍一下枚举{0,1,···,n-1}所包含的所有大小为k的子集的方法。通过使用位运算我们可以像如下代码所示简单地按照字典序升序地美剧出所有满足条件地二进制码。

int comb=(1<<k)-1;
while(comb < 1 << n){
	//这里针对组合地处理
	int x=comb&-comb,y=comb+x;
	comb=((comb&~y)/x>>1)|y;
}

按照字典序地话,最小的子集是(1<<k)-1,所以用它作为初始值。现在我们求出comb其后地二进制码,例如0101110之后地是0110011(这里说的是下一个大小为k的二进制码)。下面是求出comb下一个二进制码的方法。

  1. 求出最低为的1开始的连续的1的区间(0101110->0001110)
  2. 将这一区间全部变为0,并将区间左侧的那个0变为1(0101110->0110000)
  3. 将第1步取出的区间右移,直到剩下得1的个数变少了1个(0001110->0000011)
  4. 将第2步和第3步的结果按位异或(0110000|0000011=0110011)
    对于非0的整数,x&(-x)的值其实就是将其最低位的1独立出来后的值:
    x x的二进制 -x的二进制 x&-x
    1 0001 1111 0001
    2 0010 1110 0010
    3 0011 1101 0001
    4 0100 1100 0100
    5 0101 1011 0001
    6 0110 1010 0010
    将最低位的1取出后,设它为x,那么通过计算y=comb+x,就将comb从最低为的1开始的连续的1都置为0了。我们来比较下y和comb,最低为1开始的连续区间在y中仍然是1,区间左侧的那个0在y中仍然是0于是通过计算z=comb&y就得到了最低位1开始的连续区间。
    同时y也恰好是第2步妖气的值,那么先把z不断右移,直到最低位为1,这通过z/x就可以完成,在右移1位就可以得到第3步的值,最后再与y进行或运算,就可以求得下一个k大小的子集了。

折半枚举 (双向搜索) 4 Values whose Sum is 0 POJ No.2785

朴素算法位O(N4),但我们可以拆分位a+b=-c-d的情况,将-c-d的情况进行保存然后排序,利用二分函数进行查找。

int n;
int a[4005],b[4005],c[4005],d[4005],cd[4005*4005];
int main(){
	cin >> n;
	for(int i=0;i<n;i++){
		cin >> a[i]; 
	}
	for(int i=0;i<n;i++){
		cin >> b[i]; 
	}
	for(int i=0;i<n;i++){
		cin >> c[i]; 
	}
	for(int i=0;i<n;i++){
		cin >> d[i]; 
	}
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			cd[i*n+j]=c[i]+d[j];
		}
	}
	sort(cd,cd+n*n);
	long long res=0;
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			int e=-a[i]-b[j];
			res+=upper_bound(cd,cd+n*n,e)-lower_bound(cd,cd+n*n,e);
		}
	}
	cout << res << endl;
	return 0;
} 

复杂度为O(n2logn)

超大背包问题

1 <= n <= 40
1 <= wi,vi <=1015
1 <= W <= 1015
使用DP求解背包的复杂度是O(nW),用于这道题会TLE。所以我们应该用n比较小的特点来寻找其他办法。
挑选物品的方法总共有2n种,所以不能直接枚举,但是像前面一样拆成两半之后再枚举的话,因为每部分只有20个,所以是可行的。利用拆成两半后的两部分的价值和重量,我们把前半部分种的选取方法对应的重量和价值总和记位w1,v1。这样再后半部分寻找总重w2<=W-w1时使v2最大的选取方法就好了。
因此我们要思考从枚举得到的(w2,v2)的集合中高效寻找max{v1|w2<=W’}的方法。首先,显然我们可以排除所有w2[i]<=w2[j]并且v2[i]>=v2[j]的j。这一点可以按照w2、v2的字典序简单左到。此后剩余的元素都满足w2[i]<w2[j]<->v2[i]<v2[j],要计算max{v2|w2<=W’}的话只要寻找满足w2[i]<=W’的最大的i就可以了。这可以用二分搜索完成,剩余元素个数为M的话,一次搜索需要O(logM)的时间。O(2(n/2)n)的复杂度。

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<cmath>
using namespace std;
int n;
long long w[45],v[45],W;
pair <long long,long long> ps [1 << 41/2];//枚举前2^(n/2)种可能 
long long INF=1e15+5;
void solve(){
	int n2=n/2;
	for(int i=0;i< 1 << n2;i++){
		long long sw=0,sv=0;
		for(int j=0;j<n2;j++){
			if(i>>j&1)//判断j是否在i这个集合中 
			{
				sw+=w[j];
				sv+=v[j];
			}
		}
		ps[i]=make_pair(sw,sv);
	}
	sort(ps,ps+(1<<n2));
	//去除多余的元素
	int m=1;
	for(int i=1;i<1<<n2;i++){
		if(ps[m-1].second<ps[i].second){//如果w2[i]<=w2[j]并且v2[i]>=v2[j]则把j跳过 
			ps[m++]=ps[i];
		} 
	}
	long long res=0;
	for(int i=0;i< 1 << (n-n2);i++){
		long long sw=0,sv=0;
		for(int j=0;j<n-n2;j++){
			if(i >> j & 1){
				sw+=w[n2+j];
				sv+=v[n2+j];
			}
		}
		if(sw<=W){
			long long tv=(lower_bound(ps,ps+m,make_pair(W-sw,INF))-1)->second;//对于所有小于W-sw的状态,即符合W-sw的条件 
			res=max(res,sv+tv);//lower_bound是找第一个>=k的元素,那么-1就是最后一个<k的元素了 
		}
	} 
	cout << res << endl;
}
int main(){
	cin >> n;
	for(int i=0;i<n;i++){
		cin >> w[i];
	}
	for(int i=0;i<n;i++){
		cin >> v[i];
	}
	cin >> W;
	solve();
	cout << (1<<2) << endl; 
	return 0;
}

在这里我们用二进制来表示集合的子集,用0表示选了这个数,用1表示没有选这个数。01101表示选了序号为{2,3,5}的物品。由于最多有2n个01状态,所以我们从0位开始依次判断,如果有,那么我们把这个物品的参数加上去。

坐标离散化 :区间的个数

1 <= w,h <=1000000
1 <= n <= 500
用一般的dfs来记录区域个数的话 很明显会超空间,所以要用坐标离散化这一技巧。
在数组中只需要存储有直线的行列以及其前后的行列就够了,这样的话大小最多6n x 6n,这样我们就可以利用搜索求出区域的个数了。

int W,H,N;
const int MAXN=505;
int X1[MAXN],X2[MAXN],Y1[MAXN],Y2[MAXN];
bool fld[MAXN*6][MAXN*6];
int dx[]={0,-1,0,1};
int dy[]={1,0,-1,0};
int compress(int *x1,int * x2,int w){//对坐标进行离散化,将直线的上一行和下一行上一列和下一列放入进行存储
	vector<int> xs;
	for(int i=0;i<N;i++){
		for(int d=-1;d<=1;d++){
			int tx1=x1[i]+d,tx2=x2[i]+d;
			if(1<=tx1&&tx1<=w)xs.push_back(tx1);
			if(1<=tx2&&tx2<=w)xs.push_back(tx2);
		}
	}
	sort(xs.begin(),xs.end());
	xs.erase(unique(xs.begin(),xs.end()),xs.end());//去重,unique将重复的元素放至末尾并且返回最后一个未重复元素的迭代器 
	for(int i=0;i<N;i++){
		x1[i]=find(xs.begin(),xs.end(),x1[i])-xs.begin(); 
		x2[i]=find(xs.begin(),xs.end(),x2[i])-xs.begin();
	}
	return xs.size();
}
void solve(){
	W=compress(X1,X2,W);
	H=compress(Y1,Y2,H);
	memset(fld,0,sizeof(fld));
	for(int i=0;i<N;i++){
		for(int y=Y1[i];y<=Y2[i];y++){
			for(int x=X1[i];x<=X2[i];x++){
				fld[y][x]=true;
			}
		}
	}
	int ans=0;
	for(int y=0;y<H;y++){
		for(int x=0;x<W;x++){
			if(fld[y][x]){
				continue;
			}
			ans++;
			queue<pair<int,int> >que;
			que.push(make_pair(x,y));
			while(!que.empty()){
				int sx=que.front().first,sy=que.front().second;
				que.pop();
				for(int i=0;i<4;i++){
					int tx=sx+dx[i],ty=sy+dy[i];
					if(tx<0||ty<0||tx>=W||ty>=H)continue; 
					if(fld[ty][tx])continue;
					que.push(make_pair(tx,ty));
					fld[ty][tx]=true;
				}
			}
		}
	}
	cout << ans << endl;
}
int main(){
	cin >> W >> H >> N;
	for(int i=0;i<N;i++){
		cin >> X1[i];
	}
	for(int i=0;i<N;i++){
		cin >> X2[i];
	}
	for(int i=0;i<N;i++){
		cin >> Y1[i];
	}
	for(int i=0;i<N;i++){
		cin >> Y2[i];
	}
	solve();
	return 0;
} 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值