import std.stdio, std.string, std.conv, std.range;
import std.algorithm, std.array, std.typecons, std.container;
import std.math, std.numeric, std.random, core.bitop;

void scan(T...)(ref T args) {
    auto line = readln.split;
    foreach (ref arg; args) {
        arg = line.front.to!(typeof(arg));
        line.popFront;
    }
    assert(line.empty);
}

void fillAll(R, T)(ref R arr, T value) {
    static if (is(typeof(arr[] = value))) {
        arr[] = value;
    }
    else {
        foreach (ref e; arr) {
            fillAll(e, value);
        }
    }
}

bool chmin(T, U...)(ref T x, U args) {
    bool isChanged;
    foreach (arg; args) if (x > arg) {
        x = arg;
        isChanged = true;
    }
    return isChanged;
}

bool chmax(T, U...)(ref T x, U args) {
    bool isChanged;
    foreach (arg; args) if (x < arg) {
        x = arg;
        isChanged = true;
    }
    return isChanged;
}


enum inf = 1_001_001_001;
enum infl = 1_001_001_001_001_001_001L;


struct UnionFind {
    int N;
    int[] parent; // 根は -(連結成分のサイズ) としておく
    int cc; // 連結成分の個数

    this(int n)
    in {
        assert(n > 0);
    }
    body {
        N = n;
        cc = n;
        parent = new int[](N);
        parent[] = -1;
    }

    int root(int x)
    in {
        assert(0 <= x && x < N);
    }
    body {
        if (parent[x] < 0) return x;
        else return parent[x] = root(parent[x]);
    }

    int size(int x)
    in {
        assert(0 <= x && x < N);
    }
    body {
        return -parent[root(x)];
    }

    bool same(int x, int y)
    in {
        assert(0 <= x && x < N);
        assert(0 <= y && y < N);
    }
    body {
        return root(x) == root(y);
    }

    bool merge(int x, int y)
    in {
        assert(0 <= x && x < N);
        assert(0 <= y && y < N);
    }
    body {
        import std.algorithm : swap;

        x = root(x);
        y = root(y);

        if (x == y) return false;
        if (size(x) < size(y)) swap(x, y);

        // size(x) >= size(y)
        parent[x] += parent[y];
        parent[y] = x;
        cc--;

        return true;
    }
}


unittest {
    auto uf = UnionFind(10);

    uf.merge(0, 8);
    uf.merge(2, 7);
    uf.merge(3, 6);

    assert(!uf.same(0, 6));
    assert(!uf.same(4, 5));
    assert(uf.same(2, 7));
    assert(uf.size(8) == 2);
    assert(uf.size(2) == 2);
    assert(uf.size(3) == 2);
    assert(uf.size(4) == 1);

    uf.merge(2, 6);
    uf.merge(0, 5);
    uf.merge(0, 9);

    assert(uf.same(8, 9));
    assert(uf.same(9, 5));
    assert(uf.same(6, 7));
    assert(uf.same(7, 3));
    assert(uf.size(2) == 4);
    assert(uf.size(6) == 4);
    assert(uf.size(5) == 4);
    assert(uf.size(8) == 4);
    assert(uf.size(9) == 4);
    assert(uf.size(1) == 1);

    assert(!uf.same(1, 9));
    assert(!uf.same(5, 7));
    assert(!uf.same(0, 3));
    assert(!uf.same(8, 2));
}

alias Edge = Tuple!(int, "s", int, "t", double, "cost");

double kruskal(int N, Edge[] es) {
    double res = 0;
    es.sort!"a.cost < b.cost"();
    auto uf = UnionFind(N);
    while (uf.cc > 1) {
        auto e = es.front; es.popFront();
        auto u = e.s, v = e.t, c = e.cost;
        if (!uf.same(u, v)) {
            uf.merge(u, v);
            res += c;
        }
    }
    return res;
}

void main() {
    while (true) {
        int N;
        scan(N);
        if (N == 0) return;
        auto x = new double[](N);
        auto y = new double[](N);
        auto z = new double[](N);
        auto r = new double[](N);
        iota(N).each!(i => scan(x[i], y[i], z[i], r[i]));

        auto es = new Edge[](N * (N - 1) / 2);
        int idx;
        foreach (i ; 0 .. N) {
            foreach (j ; i + 1 .. N) {
                auto d = hypot(hypot(x[i] - x[j], y[i] - y[j]), z[i] - z[j]);
                es[idx] = Edge(i, j, max(d - r[i] - r[j], 0));
                idx++;
            }
        }
        auto ans = kruskal(N, es);
        writefln("%.3f", ans);
    }
}



Measured memory_bytes: 4348