枚举与暴力在某些题中的神奇应用

怎么枚举

字面意思,枚举可能出现的各种情况。

example

枚举全排列

枚举某一种错误的情况

枚举是否被标记过及标记的数量

枚举初始情况

为什么枚举

1. 1. 1.骗分(使用搜索算法过小数据)

2. 2. 2.数据范围很小,可以打表

3. 3. 3.正解(具体方法将在下文讲到)

什么时候枚举

1. 1. 1.数据某一个参数的范围很小

2. 2. 2.枚举后可以 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)做。

3. 3. 3.不枚举的话没有任何最优解

具体每一种思路的实现

枚举全排列

典例:搬迁

题目大意:一张图,有 k k k个特殊点 ( 1 ≤ k ≤ 5 ) (1\le k\le 5) (1k5),要求选一个点,使得从这个点出发,经过所有的特殊点,再回到该点的距离最小。

解析:观察到 k k k的范围极小,故我们可以想到枚举经过 k k k个点的顺序,再枚举每一个初始点,跑单源最短路径。

但是对每一个初始点跑单源最短路并不现实,所以我们对每个特殊点跑一遍单源最短路,枚举全排列 a [ ] a[] a[],在枚举我们的初始点,设初始点为 s s s,特殊点为 t 1 , t 2 , . . . , t k t_1,t_2,...,t_k t1,t2,...,tk,特殊点 k i k_i ki到每个点的最短路为 d [ i ] [ ] d[i][] d[i][]那么我们的答案就为

d [ a [ 1 ] ] [ s ] + d [ a [ 1 ] ] [ a [ 2 ] ] + d [ a [ 2 ] ] [ a [ 3 ] ] + . . . + d [ a [ k − 1 ] ] [ a [ k ] ] + d [ a [ k ] ] [ s ] d[a[1]][s]+d[a[1]][a[2]]+d[a[2]][a[3]]+...+d[a[k-1]][a[k]]+d[a[k]][s] d[a[1]][s]+d[a[1]][a[2]]+d[a[2]][a[3]]+...+d[a[k1]][a[k]]+d[a[k]][s]
更新最大值即可。

#include<bits/stdc++.h>
#define int long long
using namespace std;
int first[100005],nxt[100005],to[100005],w[100005],tot=0,ans=2147483647;
int n,m,k,d[6][100005],vis[100005],a[100005],s[15],VIS[15];
int Read(){
	int x;
	scanf("%lld",&x);
	return x;
}
void Add(int x,int y,int z){
	nxt[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
	w[tot]=z;
}
void dijkstra(int f){
	priority_queue<pair<int,int> > q;
	memset(vis,0,sizeof(vis));
	d[f][a[f]]=0;
	q.push(make_pair(0,a[f]));
	while(!q.empty()){
		int u=q.top().second;
		q.pop();
		if(vis[u]==1)  continue;
		vis[u]=1;
		for(int e=first[u];e;e=nxt[e]){
			int v=to[e];
			if(d[f][v]>d[f][u]+w[e]){
				d[f][v]=d[f][u]+w[e];
				q.push(make_pair(-d[f][v],v));
			}
		}
	}
}
void check(){
	int dis=2147483647;
	for(int i=1;i<=n;i++){
		if(i==a[1]||i==a[2]||i==a[3]||i==a[4]||i==a[5])  continue;
		int q=d[s[1]][i];
		for(int j=2;j<=k;j++){
			q+=d[s[j]][a[s[j-1]]];
		}
		q+=d[s[k]][i];
		//cout<<i<<" "<<q<<" "<<s[1]<<" "<<s[2]<<" "<<s[3]<<endl;
		dis=min(dis,q);
	}
	ans=min(ans,dis);
}
void dfs(int x){
	if(x==k+1){
		check();
		return ;
	}
	for(int i=1;i<=k;i++){
		if(!VIS[i]){
			VIS[i]=1;
			s[x]=i;
			dfs(x+1);
			VIS[i]=0;
			s[x]=0;
		}
	}
}
signed main(){
	memset(d,0x7f,sizeof(d));
	n=Read(),m=Read(),k=Read();
	for(int i=1;i<=k;i++){
		a[i]=Read();
	}
	for(int i=1;i<=m;i++){
		int x=Read(),y=Read(),z=Read();
		Add(x,y,z);
		Add(y,x,z);
	}
	for(int i=1;i<=k;i++){
		dijkstra(i);
	}
	/*for(int i=1;i<=k;i++){
		for(int j=1;j<=n;j++){
			cout<<d[i][j]<<" ";
		}
		cout<<endl;
	}*/
	dfs(1);
	cout<<ans<<endl;
}
枚举每一种错误的情况

典例:Barracuda

题目大意:有 n n n个物品 ( n ≤ 100 ) (n\le 100) (n100),每个物品有一个重量,有 n + 1 n+1 n+1次称量结果,每次称一些物品,称出重量为 w w w,其中有一次是错误的,问是否有唯一确定解和最重的物品编号是多少。

考虑没有错误的情况,那么我们的题意就是求解关于 n n n个未知数的 n n n个方程,可以高斯消元 O ( n 3 ) O(n^3) O(n3)做,但现在有一个错误方程,我们容易想到 O ( n ) O(n) O(n)枚举错误的方程,由于 n ≤ 100 n\le100 n100 O ( n 4 ) O(n^4) O(n4)刚好可以卡过。

#include<bits/stdc++.h>
using namespace std;
double a[105][105],b[105][105],w[105];
int flag=0,m[105],f[105][105],n,Num,k,x;
int solve(){
	for(int i=1;i<=n;i++){
		int maxn=i;
		for(int j=i+1;j<=n;j++){
			if(fabs(a[j][i])>fabs(a[maxn][i]))
			  maxn=j;
		}
		for(int j=1;j<=n+1;j++){
			swap(a[i][j],a[maxn][j]);
		}
		if(!a[i][i])  return 0;
		for(int j=1;j<=n;j++){
			if(j==i)  continue;
			double tmp=a[j][i]/a[i][i];
			for(int k=i+1;k<=n+1;k++){
				a[j][k]-=a[i][k]*tmp;
			}
		}
	}
	int maxn=-1,num=0;
	for(int i=1;i<=n;i++){
		double ans=a[i][n+1]/a[i][i];
		if(ans<=0)  return 0;
		if(ans!=(int)ans)  return 0;
		maxn=max(maxn,(int)ans);
	}
	for(int i=1;i<=n;i++){
		int ans=a[i][n+1]/a[i][i];
		if(maxn==ans){
			if(num)  return 0;
			num=i;
		}
	}
	return num;
	
}
int main(){
	scanf("%d",&n);
	for(int i=1;i<=n+1;i++){
		scanf("%d",&k);
		for(int j=1;j<=k;j++){
			scanf("%d",&x);
			b[i][x]=1;
		}
		scanf("%lf",&b[i][n+1]);
	}
	for(int i=1;i<=n+1;i++){
		swap(b[i],b[n+1]);
		for(int j=1;j<=n;j++){
			for(int k=1;k<=n+1;k++){
				a[j][k]=b[j][k];
			}
		}
		swap(b[i],b[n+1]);
		Num=solve();
		if(Num){
			if(flag){
				cout<<"illegal\n";
				return 0;
			}
			flag=Num;
		}
	}
	if(!flag){
		cout<<"illegal\n";
		return 0;
	}
	else  cout<<flag<<endl;
}
枚举是否被标记过及标记的数量

典例:道路

题目大意:给定一棵树,有 n n n个叶子结点,每个结点到其父亲结点都有两条路,分别为 A A A型和 B B B型。要求对每个结点到其父亲结点的两条路中选择一条进行翻修。每个结点有一个参数 ( a i , b i , c i ) (a_i,b_i,c_i) (ai,bi,ci),设每个叶子结点到根结点的路上有 x x x条未翻修的 A A A路径和 y y y条未翻修的 B B B路径,求 ∑ i = 1 n c i ( a i + x ) ( b i + y ) \sum_{i=1}^nc_i(a_i+x)(b_i+y) i=1nci(ai+x)(bi+y)的最小值。

解析:设 f [ u ] [ i ] [ j ] f[u][i][j] f[u][i][j]表示从 u u u到根有 i i i条未翻修的 A A A边与 j j j条未翻修的 B B B

如果 u u u是叶结点,那么我们枚举 i , j i,j i,j,有
f [ u ] [ i ] [ j ] = c u ( a u + i ) ( b u + j ) f[u][i][j]=c_u(a_u+i)(b_u+j) f[u][i][j]=cu(au+i)(bu+j)
如果 u u u不是叶结点,我们枚举删哪条边,有
f [ u ] [ i ] [ j ] = m i n ( f [ l s o n ] [ i + 1 ] [ j ] + f [ r s o n ] [ i ] [ j ] , f [ l s o n ] [ i ] [ j ] + f [ r s o n ] [ i ] [ j + 1 ] ) f[u][i][j]=min(f[lson][i+1][j]+f[rson][i][j],f[lson][i][j]+f[rson][i][j+1]) f[u][i][j]=min(f[lson][i+1][j]+f[rson][i][j],f[lson][i][j]+f[rson][i][j+1])
稍微注意细节即可。

#include<bits/stdc++.h>
using namespace std;
int Read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')  f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+ch-'0';
		ch=getchar();
	}
	return x*f;
}
int n,a[40005],b[40005],c[40005];
int first[80005],nxt[80005],to[80005],tot=0;
void Add(int x,int y){
	nxt[++tot]=first[x];
	first[x]=tot;
	to[tot]=y;
}
vector<int> g[40005];
int cnt,size[40005],dep[40005],dfn[40005];
long long f[105][45][45];
void dfs(int u,int k){
	dfn[u]=k;
	size[u]=1;
	for(int e=first[u];e;e=nxt[e]){
		int v=to[e];
		dep[v]=dep[u]+1;
		g[u].push_back(v);
		if(g[u][1])  dfs(v,k+2);
		else  dfs(v,k+1);
		size[u]+=size[v];
	}
	if(size[u]==1){
		for(int i=0;i<=dep[u];i++){
			for(int j=0;j<=dep[u];j++){
				f[dfn[u]][i][j]=1ll*c[u]*(a[u]+i)*(b[u]+j);
			}
		}
	}
	else{
		for(int i=0;i<=dep[u];i++){
			for(int j=0;j<=dep[u];j++){
				f[dfn[u]][i][j]=min(f[dfn[g[u][0]]][i][j]+f[dfn[g[u][1]]][i][j+1],f[dfn[g[u][0]]][i+1][j]+f[dfn[g[u][1]]][i][j]);
			}
		}
	}
}
int main(){
	memset(f,63,sizeof(f));
	n=Read();
	for(int i=1;i<n;i++){
		int x=Read(),y=Read();
		if(y<0)  Add(i,-y+n-1);
		else  Add(i,y);
		if(x<0)  Add(i,-x+n-1);
		else  Add(i,x);
	}
	for(int i=n;i<=2*n-1;i++){
		a[i]=Read(),b[i]=Read(),c[i]=Read();
	}
	dep[1]=0;
	dfs(1,0);
	printf("%lld",f[dfn[1]][0][0]);
}
枚举初始情况

典例:翻转棋

题目大意:给一个 n ∗ n n*n nn的矩阵 ( n ≤ 16 ) (n\le16) (n16),里面有 0 , 1 0,1 0,1两种元素,你可以翻转一个格子及其上下左右的格子,问一个字典序最小的解。

解析:直接处理会很困难,我们考虑如果答案矩阵第一行的状态已知,我们可以 O ( n 2 ) O(n^2) O(n2)算出整个答案矩阵的状态,故我们枚举第一行的状态,然后判断是否可行。

#include<bits/stdc++.h>
using namespace std;
int Read(){
	int x=0,f=1;
	char ch=getchar();
	while(!isdigit(ch)){
		if(ch=='-')  f=-1;
		ch=getchar();
	}
	while(isdigit(ch)){
		x=(x<<3)+(x<<1)+ch-'0';
		ch=getchar();
	}
	return x*f;
}
int n,m,a[25][25],b[25][25],c[25][25],num[25],ans[25][25],flag=0,Ans=2147483647;
void Copy(){
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			ans[i][j]=c[i][j];
		}
	}
}
void check(){
	memset(c,0,sizeof(c));
	int cnt=0;
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			b[i][j]=a[i][j];
		}
	}
	for(int i=1;i<=m;i++){
		if(num[i]){
			b[1][i]^=1;
			b[2][i]^=1;
			if(i>1)  b[1][i-1]^=1;
			if(i<m)  b[1][i+1]^=1;
			cnt++;
		}
		c[1][i]=num[i];
	}
	for(int i=2;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(b[i-1][j]){
				cnt++;
				c[i][j]=1;
				b[i-1][j]^=1;
				b[i][j]^=1;
				if(j>1)  b[i][j-1]^=1;
				if(j<m)  b[i][j+1]^=1;
				if(i<n)  b[i+1][j]^=1;
			}
		}
	}
	for(int i=1;i<=m;i++){
		if(b[n][i]!=0)  return ;
	}
	if(cnt>Ans)  return;
	if(cnt<Ans){
		Ans=cnt;
		Copy();
		flag=1;
		return ;
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			if(c[i][j]>ans[i][j])  return ;
			if(c[i][j]<ans[i][j]){
				Copy();
				flag=1;
				return ;
			}
		}
	}
}
void dfs(int x){
	if(x==m+1){
		check();
		return ;
	}
	num[x]=0;
	dfs(x+1);
	num[x]=1;
	dfs(x+1);
}
signed main(){
	//freopen("fliptile.in","r",stdin);
	//freopen("fliptile.out","w",stdout);
	n=Read(),m=Read();
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			a[i][j]=Read();
		}
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			ans[i][j]=2;
		}
	}
	dfs(1);
	if(!flag){
		cout<<"IMPOSSIBLE\n";
		fclose(stdin);
		fclose(stdout);
	}
	for(int i=1;i<=n;i++){
		for(int j=1;j<=m;j++){
			cout<<ans[i][j]<<" ";
		}
		cout<<endl;
	}
	//fclose(stdin);
	//fclose(stdout);
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值