1. 程式人生 > >帶有技巧的搜尋(洛谷,數獨二進位制優先找列舉順序,旅行商(寫了狀壓DP),數字三角(利用楊輝三角的係數),滑雪(記憶化))

帶有技巧的搜尋(洛谷,數獨二進位制優先找列舉順序,旅行商(寫了狀壓DP),數字三角(利用楊輝三角的係數),滑雪(記憶化))

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)