给定 n n n个村庄的坐标 ( n < = 1000 ) \left(n <= 1000 \right) (n<=1000)与高度,两个村庄之间的距离是两点的欧式距离, 两个村庄修一条路需要花费两个村庄高度差的价钱,现在你要修若干条路,让任意一个村庄总能通过一条路到另一个村庄, 求出道路总成本与总长度比值的最小值。
很明显是0/1分数规划, 题意是选出一棵树使得道路总成本与总长度比值最小,设 x i ∈ ( 0 , 1 ) x_i \in \left( 0, 1 \right) xi∈(0,1) 表示第i条路选或者不选, 选就是 x i = 1 x_i = 1 xi=1,不选就是 x i = 0 x_i = 0 xi=0, m m m为总边数, p i p_i pi表示第 i i i条边的成本, l i l_i li表示第 i i i条边的长度.若答案是 a n s ans ans, 那么有 ∑ i = 0 m x i p i ∑ i = 0 m x i l i > = a n s \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} >= ans ∑i=0mxili∑i=0mxipi>=ans,即从 m m m条边中选出任意 n − 1 n - 1 n−1条边,一定会有 ∑ i = 0 m x i p i ∑ i = 0 m x i l i > = a n s \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i} >= ans ∑i=0mxili∑i=0mxipi>=ans.设我们当前要判断的答案值为 m i d mid mid,那么若存在一组解 x i x_i xi使得 ∑ i = 0 m x i p i ∑ i = 0 m x i l i < m i d \displaystyle\frac{\sum_{i = 0}^mx_i p_i}{\sum_{i= 0}^mx_il_i}
由于分式不好求解,我们将上式移项化简得: ∑ i = 0 m x i p i − m i d ∗ x i l i > = 0 \sum_{i = 0}^mx_ip_i - mid *x_il_i >=0 ∑i=0mxipi−mid∗xili>=0,其中 x i x_i xi有 n − 1 n - 1 n−1项为 1 1 1,其余项为 0 0 0,若上式最小值小于 0 0 0,说明对上式任意的组合,其值都小于0,反之亦成立.我们建一张新图,设新图边权为 x i p i − m i d ∗ l i x_ip_i - mid *l_i xipi−mid∗li,上式最小值可由 P r i m Prim Prim算法或者 K r u s k a l Kruskal Kruskal算法求出
二分答案的上界为 N N N,边数为 n 2 n^2 n2,点数为 n n n,若使用 K r u s k a l Kruskal Kruskal算法,其复杂度为 O ( 2 n 2 l o g ( n ) l o g ( N ) ) O(2n^2log(n)log(N)) O(2n2log(n)log(N)),最坏情况下约为 5 e 8 5e8 5e8,在没有卡常的情况下很难通过.
若使用堆优化的 P r i m Prim Prim算法,其复杂度为 O ( n 2 l o g ( n ) l o g ( N ) ) O(n^2log(n)log(N)) O(n2log(n)log(N)),依然很难通过.
若使用朴素的 P r i m Prim Prim算法,其复杂度为 O ( n 2 l o g ( N ) ) O(n^2log(N)) O(n2log(N)),最坏情况下约为 2 e 7 2e7 2e7,比上述算法快了十倍左右.
经过上述分析发现在本题中出现了越优化算法越慢的情况,反而朴素的 P r i m Prim Prim算法更优.
代码如下
#include
#include
#include
using namespace std;
int n, m;
const int N = 1e6 + 5;
struct Edge{
int a, b;
double w;
bool operator < (const Edge& t) const{
return w > t.w;
}
}ed[N];
int price[1005][1005];
double Dist[1005][1005], d[1005];
bool st[1005];
inline double dist(int x, int y){
if(Dist[x][y]) return Dist[x][y];
return Dist[x][y] = sqrt((double)(ed[x].a - ed[y].a) * (ed[x].a - ed[y].a) + (ed[x].b - ed[y].b) * (ed[x].b - ed[y].b));
}
inline double get(int i, int j, double mid){
return price[i][j] - mid * dist(i, j);
}
bool check(double mid){
double res = 0;
memset(st, 0, sizeof st);
for(int i = 0;i < n; i++) d[i] = 1e18;
// cout << d[1] << endl;
d[0] = 0;
for(int i = 0;i < n; i++){
int t = -1;
for(int j = 0;j < n; j++) if(!st[j] && (t == -1 || d[t] > d[j])) t = j;
res += d[t];
st[t] = 1;
for(int i = t + 1;i < n; i++) {
double D = get(t, i, mid);
if(d[i] > D) {
d[i] = D;
}
}
for(int i = 0;i < t; i++) {
double D = get(i, t, mid);
if(d[i] > D) {
d[i] = D;
}
}
}
return res <= 0;
}
void read(){
for(int i = 0;i < n; i++){
int a, b;
double c;
scanf("%d%d%lf", &a, &b, &c);
ed[i].a = a, ed[i].b = b, ed[i].w = c;
}
for(int i = 0;i < n; i++)
for(int j = i + 1;j < n; j++) price[i][j] = abs(ed[i].w - ed[j].w);
double l = 0, r = 1e7;
while(r - l > 1e-4){
double mid = (l + r) / 2;
if(check(mid)) r = mid;
else l = mid;
}
printf("%.3lf\n", r);
}
void solve(){
memset(Dist, 0, sizeof Dist);
read();
}
int main(){
while(scanf("%d", &n), n) solve();
return 0;
}
但是二分在不卡常时会超时.于是想到另一种0/1分数规划的算法, D i n k e l b a c h Dinkelbach Dinkelbach算法,该算法的复杂度在此题中最坏为 O ( 3 n 2 l o g ( n ) ) O(3n^2log(n)) O(3n2log(n)),但是实际上绝大多数情况到不了这个复杂度,总体比二分会快不少.二分最坏其实也就是 1 0 7 10^7 107级别,卡常之后还要跑1500ms感觉有点玄学,也许是我时间复杂度算错了?
#include
#include
#include
#include
using namespace std;
int n, m;
const int N = 1e6 + 5;
struct Edge{
int a, b;
double w;
}ed[N];
int price[1005][1005];
double Dist[1005][1005], d[1005], len, cost;
int pre[1005];
bool st[1005];
double check(double mid){
len = 0, cost = 0;
memset(st, 0, sizeof st);
// for(int i = 0;i < n; i++) d[i] = 1e18;
// cout << d[1] << endl;
d[0] = 0;
for(int i = 1;i < n; i++) d[i] = price[0][i] - mid * Dist[0][i], pre[i] = 0;
for(int i = 0;i < n; i++){
int t = -1;
for(int j = 1;j < n; j++) if(!st[j] && (t == -1 || d[t] > d[j])) t = j;
if(t == -1) break;
if(t){
len += Dist[pre[t]][t];
cost += price[pre[t]][t];
}
st[t] = 1;
for(int i = t + 1;i < n; i++) {
double D = price[t][i] - mid * Dist[t][i];
if(d[i] > D) {
d[i] = D;
pre[i] = t;
}
}
for(int i = 1;i < t; i++) {
double D = price[i][t] - mid * Dist[i][t];
if(d[i] > D) {
d[i] = D;
pre[i] = t;
}
}
}
return cost / len;
}
void read(){
for(int i = 0;i < n; i++){
int a, b;
double c;
scanf("%d%d%lf", &a, &b, &c);
ed[i].a = a, ed[i].b = b, ed[i].w = c;
}
for(int i = 0;i < n; i++)
for(int j = i + 1;j < n; j++) price[i][j] = price[j][i] = abs(ed[i].w - ed[j].w),Dist[i][j] = Dist[j][i] = sqrt((double)(ed[i].a - ed[j].a) * (ed[i].a - ed[j].a) + (ed[i].b - ed[j].b) * (ed[i].b - ed[j].b));
double r = 0, rt = 0;
while(1){
rt = check(r);
if(fabs(r - rt) < 1e-5) break;
r = rt;
}
printf("%.3lf\n", r);
}
void solve(){
read();
}
int main(){
while(scanf("%d", &n), n) solve();
return 0;
}
两种方式时间的对比