帶有技巧的搜尋(洛谷,數獨二進位制優先找列舉順序,旅行商(寫了狀壓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].
求
最大值
那我們不出意外就要列舉所有解的情況了,可以直接列舉肯定是超時了。
於是我們改變列舉順序,從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)