原題連結:https://www.luogu.com.cn/problem/P1433
題意解讀:計算經過所有乳酪一次的總路徑最短,可以採用dfs、dp等方法。
解題思路:
最直接的思路是DFS,暴搜所有的路徑方案,計算最小距離,n最大是15,複雜度為15!≈10^12,必定會超時,先保證正確性,得到部分分:
50分程式碼:
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
double x[N], y[N]; //儲存乳酪座標
int path[N]; //儲存路徑是第幾個乳酪
double jl[205][205]; //儲存每兩個點之間的距離
bool flag[N]; //標記第i個乳酪是否已走過
int n;
double ans = 2e9;
double distance(double x1, double y1, double x2, double y2)
{
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
//k:第幾個乳酪,dist:當前走過的距離
void dfs(int k, double dist)
{
if(k > n)
{
ans = min(ans, dist);
return;
}
for(int i = 1; i <= n; i++)
{
if(flag[i]) continue;
flag[i] = true;
path[k] = i;
dfs(k + 1, dist + jl[path[k]][path[k-1]]);
flag[i] = false; //恢復flag
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= n; j++)
{
jl[i][j] = distance(x[i], y[i], x[j], y[j]);
}
}
dfs(1, 0);
cout << fixed << setprecision(2) << ans;
return 0;
}
能否儘量減少DFS的次數呢?由於是要找最小路徑,那麼如果在DFS過程中,某次已走過的路徑已經大於之前儲存的最小路徑,則本次dfs可以返回,這樣起到了一定的剪枝效果,關鍵程式碼是:if(dist > ans) return;
由於剪枝無法準確評估到底能減掉多少,所以複雜度是由資料點來決定的,仍然無法保證不超時,只能儘量減少超時。
90分程式碼:
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
double x[N], y[N]; //儲存乳酪座標
int path[N]; //儲存路徑是第幾個乳酪
double jl[205][205]; //儲存每兩個點之間的距離
bool flag[N]; //標記第i個乳酪是否已走過
int n;
double ans = 2e9;
double distance(double x1, double y1, double x2, double y2)
{
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
//k:第幾個乳酪,dist:當前走過的距離
void dfs(int k, double dist)
{
if(dist > ans) return; //如果某次中間的距離已經大於之前的最小ans,直接結束本次搜尋
if(k > n)
{
ans = min(ans, dist);
return;
}
for(int i = 1; i <= n; i++)
{
if(flag[i]) continue;
flag[i] = true;
path[k] = i;
dfs(k + 1, dist + jl[path[k]][path[k-1]]);
flag[i] = false; //恢復flag
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> x[i] >> y[i];
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= n; j++)
{
jl[i][j] = distance(x[i], y[i], x[j], y[j]);
}
}
dfs(1, 0);
cout << fixed << setprecision(2) << ans;
return 0;
}
還能不能進一步減少dfs的次數呢?
想象一種場景,當最多有15個點時,如果已經過a、b、c、d、e 5個點,當前在d點,走過距離是L1
下一次dfs時,也已經過a、b、c、d、e 5個點,當前在b點,走過距離是L2,
如果L2>L1,其實這時就沒有必要繼續dfs下去,因為肯定不可能是最優解。
因此,我們可以記錄每種狀態下所走過的距離,透過二進位制來表示已經過了哪幾個點
如:01011可以表示經過了1/2/4號點,用整數表示就是11,15個點用整數表示狀態不超過2^16-1 = 65535
定義陣列double dp[40000][20]; dp[i][j]表示,已經過的點是i所表示的二進位制狀態(1表示走過,0表示沒走過)、當前在j點所經過的距離,
在程式碼中,透過判斷dp[i][j]是否有值,且當前值比原值還大,則不需要繼續dfs。
100分程式碼:
#include <bits/stdc++.h>
using namespace std;
const int N = 20;
double x[N], y[N]; //儲存乳酪座標
int path[N]; //儲存路徑是第幾個乳酪
double jl[205][205]; //儲存每兩個點之間的距離
bool flag[N]; //標記第i個乳酪是否已走過
double dp[65540][20]; //dp[i][j]表示已經過的點是i所表示的狀態,當前在j點所經過的距離
int n;
double ans = 2e9;
double distance(double x1, double y1, double x2, double y2)
{
return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}
//k:第幾個,s:當前已經過的點的狀態,dist:當前走過的距離
void dfs(int k, int s, double dist)
{
if(dist > ans) return; //如果某次中間的距離已經大於之前的最小ans,直接結束本次搜尋
if(k > n)
{
ans = min(ans, dist);
return;
}
for(int i = 1; i <= n; i++) //遍歷所有乳酪編號
{
if(flag[i]) continue;
int ns = s + (1 << (i - 1)); //當前已經過的點算上i
double ndist = dist + jl[i][path[k-1]]; //已經過的距離加上當前距離
if(dp[ns][i] != 0 && ndist >= dp[ns][i]) continue; //如果當前已經過點的狀態之前已出現過,且距離比之前還大或相等,則不需要繼續dfs
flag[i] = true; path[k] = i; dp[ns][i] = ndist;
dfs(k + 1, ns, ndist);
flag[i] = false; //恢復flag
}
}
int main()
{
cin >> n;
for(int i = 1; i <= n; i++) cin >> x[i] >> y[i];
//預計算所有點之間的距離
for(int i = 0; i <= n; i++)
{
for(int j = 0; j <= n; j++)
{
jl[i][j] = distance(x[i], y[i], x[j], y[j]);
}
}
dfs(1, 0, 0);
cout << fixed << setprecision(2) << ans;
return 0;
}