带有技巧的搜索(洛谷,数独二进制优先找枚举顺序,旅行商(写了状压DP),数字三角(利用杨辉三角的系数),滑雪(记忆化))

版权声明:转载请在原文附上源连接以及作者,谢谢~ https://blog.csdn.net/weixin_39778570/article/details/83717709

ACM题集:https://blog.csdn.net/weixin_39778570/article/details/83187443
P1118 [USACO06FEB]数字三角形Backward Digit Su
题目:https://www.luogu.org/problemnew/show/P1118
题意:寻找满足数字三角形的字典序最小的序列
解法:根据杨辉三角我们可以知道,第一行的系数为 C(n,1),C(n,2),C(n,3)。。。
于是我们只要从小到大枚举全排列就行了,但是n会达到12,所以需要剪枝,当枚举超过答案的时候我们及时剪枝(称为不可能剪枝)

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
int n,sum,a[15];
ll cnt,nowcnt=0;
bool vis[15];
ll b[15],yh[15][15];
void PF(){
	fo(i,1,n){
		printf("%d%c",a[i],i==n?'\n':' ');
	}
	exit(0); 
}
void dfs(int step, int ans){
	if(ans>sum)return;// 不可能剪枝,这个剪枝可以剪掉很多!!! 
	if(step>n){
		nowcnt++;
		if(ans==sum)PF();
		if(nowcnt>cnt)exit(0); // 全排列是左右对称的,超过了就不用枚举了 
		return;
	}
	fo(i,1,n){
		if(!vis[i]){
			vis[i] = 1;
			a[step] = i;
			dfs(step+1, ans+yh[n][step]*i);// 杨辉三角
			vis[i] = 0;
		}
	}
}
void solve(){
	cnt = 1;
	fo(i,1,n)cnt *= n;
	cnt = cnt/2+5;
	// 数塔问题可以考虑杨辉三角优化,组合问题 
	yh[1][1]=1;
	fo(i,2,12)fo(j,1,12){
		yh[i][j] = yh[i-1][j-1] + yh[i-1][j];
	}
	dfs(1,0);
}
int main(){
	scanf("%d%d",&n,&sum);
	solve();
	return 0;
} 

P1434 [SHOI2002]滑雪
题目:https://www.luogu.org/problemnew/show/P1434
题意:找滑雪路径最长的,没次只能从高处往4周围底处滑,不知道起点
解法:把每一个(x,y)看做一个节点。高的为父节点,底的为子节点。子节点拥有多个父节点, 父节点拥有多个子节点,但不存在环,类树形结构。
避免重复计算子节点(即子节点已经计算过了,不需要重复计算,因为不存在环,
所以子节点的计算是向下的(向它的子节点计算),所以无论什么时候算,答案都是一样的),枚举起点记忆化dfs一下

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
int n,m,a[105][105];
int dx[] = {-1,1,0,0};
int dy[] = {0,0,-1,1};
int vis[105][105];
int ans;
int dfs(int x, int y){
	int t = 0;
	if(vis[x][y])return vis[x][y];//访问过了 
	fo(i,0,3){
		int nowx=x+dx[i];
		int nowy=y+dy[i];
		if(nowx>=1&&nowx<=n&&nowy>=1&&nowy<=m && a[x][y]>a[nowx][nowy]){ // 可以访问 
			t = max(t, 1+dfs(nowx,nowy)); 
		}else t = max(t,1); // 不能访问 
	}
	return vis[x][y]=t;
}
void solve(){
	fo(x,1,n)fo(y,1,m){
		if(!vis[x][y]){
			ans = max(ans, dfs(x,y));
		}
	}
	cout<<ans<<endl;
}
int main(){
	scanf("%d%d",&n,&m);
	fo(i,1,n)fo(j,1,m){
		scanf("%d",&a[i][j]);
	}
	solve();
	return 0;
} 

P1433 吃奶酪
题目:https://www.luogu.org/problemnew/show/P1433
题意:旅行商问题
解法:可以记忆化搜索,也可以状压DP
dp[i][S] 表示从i出发遍历S(包括i)的最小花费(不用回到i)
从小到大枚举S,再枚举起点i,再枚举第二个点j,更新dp[i][S]
dp[i][S] = min(dp[i][S], dis[i][j]+dp[j][S-(1<<(i-1))]);

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; ++i)
using namespace std;
int n;
double x[20],y[20];
double dis[20][20];	
double dp[16][1<<16];
void pre(){
	fo(i,0,n)fo(j,0,n){
		dis[i][j] = sqrt((x[i]-x[j])*(x[i]-x[j]) + (y[i]-y[j])*(y[i]-y[j]));
	}
}
void solve(){
	pre();
	memset(dp,127,sizeof(dp)); // 不要头铁装逼写0x3f 
	for(int S=1; S<=(1<<n)-1; S++){ // 枚举未访问集合(包括自身),从小到大枚举 
		for(int i=1; i<=n; i++){ // 枚举起点 
			if(!(S>>(i-1))&1) continue;     // 不存在该起点 
			if(S==(1<<(i-1)))dp[i][S]=0;    // 集合就是本身
			else{ // 集合起点i,集合S(包括i) 
				int qu = (1<<(i-1));
				for(int j=1; j<=n; j++){ // 下一步 
					if(i!=j && (S>>(j-1))&1) { // 存在j这个起点 
						dp[i][S] = min(dp[i][S], dis[i][j]+dp[j][S-qu]);
					}
				}
			} 
		}
	}
	// 枚举起点 
	double min_val = 999999999;
	for(int i=1;i<=n; i++){
		if(min_val>dp[i][(1<<n)-1]+dis[0][i]){
			min_val = dp[i][(1<<n)-1]+dis[0][i];
		} 
	}
	printf("%.2f\n",min_val);
}
int main(){
	scanf("%d",&n);
	fo(i,1,n){
		scanf("%lf%lf",&x[i],&y[i]);
	}
	solve();
	return 0;
}

P1074 靶形数独
题目:https://www.luogu.org/problemnew/show/P1074
题意:要求输出数独的最高分数,数独的每个位置有个权值a[x][y],数独记为mp[x][y].
x = 1 n y = 1 n a [ x ] [ y ] m p [ x ] [ y ] \sum_{x=1}^{n}\sum_{y=1}^{n} {a[x][y]*mp[x][y]} 最大值
那我们不出意外就要枚举所有解的情况了,可以直接枚举肯定是超时了。
于是我们改变枚举顺序,从81个格子里边找到一个格子可以填充的数最少的那个格子(已经填过的除外),同时如果发现某个格子没有数可以填,那么当前这种填法是错误的,剪枝回溯当上一层继续搜索。
这样枚举的好处是,因为每次,每层填的都是可填数最少的数,那么这个格子很有可能很容易就被填对了,而且该层的分支也会很少,尽可能地先枚举少分支且正确的分支,使得后面的分支(越往后的层每层的分支递增速度非常快)在比较前面就被剪掉了,减少了很多分支。
找格子可以填的数,我们可以使用三个二进制数,分别表示行,列,当前网格(3*3),第i位为1则表示该行/列/网格可以填i,我们只需要求一下异或和就行。值中为1的表示可以填。

#include<bits/stdc++.h>
#define ll long long
#define fo(i,j,n) for(register int i=j; i<=n; i++)
using namespace std;
int a[10][10],mp[10][10],cnt[1<<9];
int row[9],col[9],grid[9],num[1<<9]; // 行,列,网格 
ll max_val;
void pre(){
	// 预处理成绩 
	int x,y,n=9,tot;
   	tot = a[x=0][y=0] = 6;
    while(tot<=10){ 
        while(x+1<n && !a[y][x+1]) a[y][++x] = tot;
        while(y+1<n && !a[y+1][x]) a[++y][x] = tot;
        while(x-1>=0 && !a[y][x-1]) a[y][--x] = tot;
        while(y-1>=0 && !a[y-1][x]) a[--y][x] = tot;
        tot++;
    }
    // 预处理二进制1的数目
	for(int i=0; i<(1<<9); ++i){
		for(int j=i; j; j-= -j&j){
			cnt[i]++;
		}
	}
	// 第1<<i对应的i 
	for(int i=0; i<9; ++i)num[1<<i] = i;
	// 先全不填!!!!一定要
	fo(i,0,8)row[i]=col[i]=grid[i] = (1<<9)-1; 
}
// 获得网格下标 
int g(int x, int y){// 0,1,2
	return ((x/3)*3)+y/3;
}
// 填充或者抹去 
void fill(int x,int y, int z){
	row[x] ^= 1<<z;
	col[y] ^= 1<<z;
	grid[g(x,y)] ^= 1<<z;
}
bool dfs(int last, ll sum){
	if(last==0){
		if(max_val<sum){
			max_val=sum;
		}
		return 1;
	}
	// 寻找9*9网格中某个格子可填充数最少,使得分枝减少
	// 这叫优先搜索顺序 
	int temp = 10, x, y;
	fo(i,0,8)fo(j,0,8){
		if(mp[i][j])continue;//填过了 
		int val = row[i] & col[j] & grid[g(i,j)];
		if(!val) return 0; // 没有数可以填,剪枝 
		if(cnt[val]<temp){
			temp=cnt[val];
			x = i, y = j;
		}
	} 
	bool flag = 0; // 数独是否有解 
	int val = row[x] & col[y] & grid[g(x,y)];
	for(;val; val-= val&-val){ // 枚举可填充数 
		int z = num[val&-val]; // 低价是第几位
		mp[x][y] = z+1;
		fill(x,y,z);
		if(dfs(last-1, sum+mp[x][y]*a[x][y])) flag = 1;
		fill(x,y,z);
		mp[x][y] = 0;
	}
	return flag;
} 
int main(){
	int last = 0;
	pre();
	fo(i,0,8)fo(j,0,8){
		scanf("%d",&mp[i][j]);
		if(mp[i][j]==0)last++;
		else {
			fill(i,j,mp[i][j]-1); // 1在第0位 
			max_val += a[i][j]*mp[i][j];
		}
	} 
	if(dfs(last, max_val))cout<<max_val<<endl;
	else puts("-1"); 
	return 0;
}

猜你喜欢

转载自blog.csdn.net/weixin_39778570/article/details/83717709