簡體   English   中英

計算一組關系的整數映射的更有效算法

[英]More efficient algorithm to compute an integer mapping for a set of relations

原始問題和簡單算法

給定一組關系,例如

a < c
b < c
b < d < e

找到與關系集匹配的以0開頭的整數集(以及盡可能多的重復整數!)的最有效算法是什么,即在這種情況下

a = 0; b = 0; c = 1; d = 1; e = 2

簡單的算法是重復遍歷關系集並根據需要增加值,直到達到收斂為止,如以下在Python中實現的:

relations = [('a', 'c'), ('b', 'c'), ('b', 'd', 'e')]

print(relations)
values = dict.fromkeys(set(sum(relations, ())), 0)

print(values)
converged = False
while not converged:
    converged = True
    for relation in relations:
        for i in range(1,len(relation)):
            if values[relation[i]] <= values[relation[i-1]]:
                converged = False
                values[relation[i]] += values[relation[i-1]]-values[relation[i]]+1
    print(values)

除了O(Relations²)復雜度(如果我沒記錯的話)之外,如果給出了無效的關系(例如加e <d),該算法也會陷入無限循環。 對於我的用例來說,檢測到這樣的失敗案例並不是嚴格必要的,但這將是一個不錯的選擇。

基於Tim Peter的評論的Python實現

relations = [('a', 'c'), ('b', 'c'), ('b', 'd'), ('b', 'e'), ('d', 'e')]

symbols = set(sum(relations, ()))
numIncoming = dict.fromkeys(symbols, 0)
values = {}

for rel in relations:
    numIncoming[rel[1]] += 1

k = 0
n = len(symbols)
c = 0
while k < n:
    curs = [sym for sym in symbols if numIncoming[sym] == 0]
    curr = [rel for rel in relations if rel[0] in curs]
    for sym in curs:
        symbols.remove(sym)
        values[sym] = c
    for rel in curr:
        relations.remove(rel)
        numIncoming[rel[1]] -= 1
    c += 1
    k += len(curs)
print(values)

目前,它需要將關系“拆分”(b <d和d <e而不是b <d <e),但是檢測循環很容易(當curs為空且k <n時),應該可以使其更有效地實施(尤其是確定curscurr方式)

最壞情況下的計時(1000個元素,999個關系,相反的順序):

Version A: 0.944926519991
Version B: 0.115537379751

最佳案例時機(1000個元素,999個關系,正序):

Version A: 0.00497004507556
Version B: 0.102511841589

平均案例時間(1000個元素,999個關系,隨機順序):

Version A: 0.487685376214
Version B: 0.109792166323

可以通過生成測試數據

n = 1000
relations_worst = list((a, b) for a, b in zip(range(n)[::-1][1:], range(n)[::-1]))
relations_best = list(relations_worst[::-1])
relations_avg = shuffle(relations_worst)

基於Tim Peter答案的C ++實現 (簡化了符號[0,n])

vector<unsigned> chunked_topsort(const vector<vector<unsigned>>& relations, unsigned n)
{
    vector<unsigned> ret(n);
    vector<set<unsigned>> succs(n);
    vector<unsigned> npreds(n);

    set<unsigned> allelts;
    set<unsigned> nopreds;

    for(auto i = n; i--;)
        allelts.insert(i);

    for(const auto& r : relations)
    {
        auto u = r[0];
        if(npreds[u] == 0) nopreds.insert(u);
        for(size_t i = 1; i < r.size(); ++i)
        {
            auto v = r[i];
            if(npreds[v] == 0) nopreds.insert(v);
            if(succs[u].count(v) == 0)
            {
                succs[u].insert(v);
                npreds[v] += 1;
                nopreds.erase(v);
            }
            u = v;
        }
    }

    set<unsigned> next;
    unsigned chunk = 0;
    while(!nopreds.empty())
    {
        next.clear();
        for(const auto& u : nopreds)
        {
            ret[u] = chunk;
            allelts.erase(u);
            for(const auto& v : succs[u])
            {
                npreds[v] -= 1;
                if(npreds[v] == 0)
                    next.insert(v);
            }
        }
        swap(nopreds, next);
        ++chunk;
    }

    assert(allelts.empty());

    return ret;
}

具有改進的緩存局部性的C ++實現

vector<unsigned> chunked_topsort2(const vector<vector<unsigned>>& relations, unsigned n)
{
    vector<unsigned> ret(n);
    vector<unsigned> npreds(n);

    vector<tuple<unsigned, unsigned>> flat_relations; flat_relations.reserve(relations.size());
    vector<unsigned> relation_offsets(n+1);

    for(const auto& r : relations)
    {
        if(r.size() < 2) continue;
        for(size_t i = 0; i < r.size()-1; ++i)
        {
            assert(r[i] < n && r[i+1] < n);
            flat_relations.emplace_back(r[i], r[i+1]);
            relation_offsets[r[i]+1] += 1;
            npreds[r[i+1]] += 1;
        }
    }
    partial_sum(relation_offsets.begin(), relation_offsets.end(), relation_offsets.begin());
    sort(flat_relations.begin(), flat_relations.end());

    vector<unsigned> nopreds;
    for(unsigned i = 0; i < n; ++i)
        if(npreds[i] == 0)
            nopreds.push_back(i);

    vector<unsigned> next;
    unsigned chunk = 0;
    while(!nopreds.empty())
    {
        next.clear();
        for(const auto& u : nopreds)
        {
            ret[u] = chunk;
            for(unsigned i = relation_offsets[u]; i < relation_offsets[u+1]; ++i)
            {
                auto v = std::get<1>(flat_relations[i]);
                npreds[v] -= 1;
                if(npreds[v] == 0)
                    next.push_back(v);
            }
        }
        swap(nopreds, next);
        ++chunk;
    }

    assert(all_of(npreds.begin(), npreds.end(), [](unsigned i) { return i == 0; }));

    return ret;
}

C ++計時 10000個元素,9999個關系,平均運行1000次

“最壞的情況下”:

chunked_topsort: 4.21345 ms
chunked_topsort2: 1.75062 ms

“最佳情況”:

chunked_topsort: 4.27287 ms
chunked_topsort2: 0.541771 ms

“平均情況”:

chunked_topsort: 6.44712 ms
chunked_topsort2: 0.955116 ms

與Python版本不同,C ++ chunked_topsort很大程度上取決於元素的順序。 有趣的是,到目前為止,隨機順序/平均情況最慢(使用基於集合的chunked_topsort )。

這是我之前沒有時間發布的實現:

def chunked_topsort(relations):
    # `relations` is an iterable producing relations.
    # A relation is a sequence, interpreted to mean
    # relation[0] < relation[1] < relation[2] < ...
    # The result is a list such that
    # result[i] is the set of elements assigned to i.
    from collections import defaultdict
    succs = defaultdict(set)    # new empty set is default
    npreds = defaultdict(int)   # 0 is default
    allelts = set()
    nopreds = set()

    def add_elt(u):
        allelts.add(u)
        if npreds[u] == 0:
            nopreds.add(u)

    for r in relations:
        u = r[0]
        add_elt(u)
        for i in range(1, len(r)):
            v = r[i]
            add_elt(v)
            if v not in succs[u]:
                succs[u].add(v)
                npreds[v] += 1
                nopreds.discard(v)
            u = v
    result = []
    while nopreds:
        result.append(nopreds)
        allelts -= nopreds
        next_nopreds = set()
        for u in nopreds:
            for v in succs[u]:
                npreds[v] -= 1
                assert npreds[v] >= 0
                if npreds[v] == 0:
                    next_nopreds.add(v)
        nopreds = next_nopreds
    if allelts:
        raise ValueError("elements in cycles %s" % allelts)
    return result

然后,例如

>>> print chunked_topsort(['ac', 'bc', 'bde', 'be', 'fbcg'])
[set(['a', 'f']), set(['b']), set(['c', 'd']), set(['e', 'g'])]

希望能有所幫助。 請注意,這里沒有任何形式的搜索(例如,沒有條件列表推導)。 從理論上講,這使它高效;-)。

稍后:計時

在帖子結尾附近生成的測試數據上, chunked_topsort()對輸入的順序幾乎不敏感。 這並不令人感到意外,因為該算法僅對輸入進行一次迭代以構建其(本來是無序的)命令和集合。 總體而言,它比Version B快15到20倍。 3次運行的典型時序輸出:

worst chunked  0.007 B  0.129 B/chunked  19.79
best  chunked  0.007 B  0.110 B/chunked  16.85
avg   chunked  0.006 B  0.118 B/chunked  19.06

worst chunked  0.007 B  0.127 B/chunked  18.25
best  chunked  0.006 B  0.103 B/chunked  17.16
avg   chunked  0.006 B  0.119 B/chunked  18.86

worst chunked  0.007 B  0.132 B/chunked  20.20
best  chunked  0.007 B  0.105 B/chunked  16.04
avg   chunked  0.007 B  0.113 B/chunked  17.32

具有更簡單的數據結構

考慮到問題已經改變了;-),這是一個重寫,假設輸入是range(n)中的整數,並且還傳遞了n 初始傳遞輸入關系后,沒有集合,沒有字典,也沒有動態分配。 在Python中,這比測試數據上的chunked_topsort()快40%。 但是我太老了,不能再和C ++搏斗了;-)

def ct_special(relations, n):
    # `relations` is an iterable producing relations.
    # A relation is a sequence, interpreted to mean
    # relation[0] < relation[1] < relation[2] < ...
    # All elements are in range(n).
    # The result is a vector of length n such that
    # result[i] is the ordinal assigned to i, or
    # result[i] is -1 if i didn't appear in the relations.
    succs = [[] for i in xrange(n)]
    npreds = [-1] * n
    nopreds = [-1] * n
    numnopreds = 0

    def add_elt(u):
        if not 0 <= u < n:
            raise ValueError("element %s out of range" % u)
        if npreds[u] < 0:
            npreds[u] = 0

    for r in relations:
        u = r[0]
        add_elt(u)
        for i in range(1, len(r)):
            v = r[i]
            add_elt(v)
            succs[u].append(v)
            npreds[v] += 1
            u = v

    result = [-1] * n
    for u in xrange(n):
        if npreds[u] == 0:
            nopreds[numnopreds] = u
            numnopreds += 1

    ordinal = nopreds_start = 0
    while nopreds_start < numnopreds:
        next_nopreds_start = numnopreds
        for i in xrange(nopreds_start, numnopreds):
            u = nopreds[i]
            result[u] = ordinal
            for v in succs[u]:
                npreds[v] -= 1
                assert npreds[v] >= 0
                if npreds[v] == 0:
                    nopreds[numnopreds] = v
                    numnopreds += 1
        nopreds_start = next_nopreds_start
        ordinal += 1
    if any(count > 0 for count in npreds):
        raise ValueError("elements in cycles")
    return result

在Python中,這再次對輸入順序不敏感。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM