Gibt es einen subkubischen Algorithmus für das folgende Problem?

11

Bei einer symmetrischen reellen Matrix gibt es einen Algorithmus, der die Summe berechnet) über alle 1 \ leq i <j <k \ leq n mit Zeitkomplexität besser als O (n ^ 3) ?n×nA=(aij)

i,j,kmax(aij,aik,ajk)
1i<j<knO(n3)
user89217
quelle
3
Beachten Sie, dass dies mindestens so schwierig ist wie das Zählen der Anzahl der Dreiecke in einem bestimmten Diagramm. Wenn Ihre Eingabematrix einen Graphen so codiert, dass "0" eine Kante und "1" eine fehlende Kante anzeigt, ist max(aij,aik,ajk)=0 genau dann, wenn vorhanden ist ein Dreieck, das durch die Knoten i , j und k , und ansonsten ist es 1 .
Jukka Suomela
1
Ich denke, die einzigen bekannten signifikant subkubischen Algorithmen für die Dreieckszählung basieren auf einer schnellen Matrixmultiplikation. Es kann schwierig sein, diese Techniken hier in diesem Problem anzuwenden. Wenn Sie nach etwas Praktischem suchen, ist alles, was auf schneller Matrixmultiplikation basiert, nicht hilfreich.
Jukka Suomela

Antworten:

3

Es gibt einen recht praktischen Ansatz, der in funktioniert , wobei die Anzahl der Bits im Prozessorwort ist. Die Hauptidee ist, dass Sie die Elemente der Matrix nacheinander in aufsteigender Reihenfolge durchlaufen (Bindungen willkürlich unterbrechen) und "einschalten". Betrachten Sie den Moment, in dem das größte Element eines Tripels ist. Nehmen wir der Einfachheit halber an, dass das besagte Element . Es ist natürlich, den Wert des Tripels jetzt zur Antwort hinzuzufügen, wenn das letzte Element eingeschaltet ist. Wir müssen also die Anzahl der möglichen , so dass undO(n3/w)waij,aik,ajkaijkaikajksind bereits eingeschaltet (das wäre die Anzahl der Tripel, hier ist das größte Element, daher wurden sie gerade vollständig eingeschaltet). Hier können wir die naive -Implementierung durch Bitoptimierung beschleunigen .aijO(n)

Einzelheiten finden Sie in der folgenden Implementierung in C ++ 11, die für , funktionieren sollte (es ist nicht sehr optimiert; es schlägt jedoch immer noch die naive Summierung für mit großem Abstand, zumindest auf meinem Computer).n5000|aij|109n=5000

// code is not very elegant, 
// but should be understandable
// here the matrix a has dimensions n x n
// a has to be symmetric!
int64_t solve (int n, const vector<vector<int32_t>> &a)
{
        std::vector<boost::dynamic_bitset<int64_t>> mat
        (n, boost::dynamic_bitset<int64_t>(n));

        vector<pair<int, int>> order;
        for (int j = 1; j < n; j++)
        for (int i = 0; i < j; i++)
            order.emplace_back(i, j);
        sort(order.begin(), order.end(),
            [&] (const pair<int, int> &l, const pair<int, int> &r) 
            {return a[l.first][l.second] < a[r.first][r.second];});

        int64_t ans = 0;
        for (const auto &position : order)
        {
            int i, j;
            tie (i, j) = position;
            mat[i][j] = mat[j][i] = 1;
            // here it is important that conditions 
            // mat[i][i] = 0 and mat[j][j] = 0 always hold
            ans += (mat[i] & mat[j]).count() * int64_t(a[i][j]);
        }

        return ans;
}

Wenn Sie in Betracht ziehen, Bitoptimierungen zu verwenden, können Sie hier die vier russische Methoden verwenden, um das gleiche Ergebnis zu erzielen. Dies ergibt einen -Algorithmus, der weniger praktisch sein sollte (da auf den meisten modernen Hardwarekomponenten ziemlich groß ist). ist aber theoretisch besser. In der Tat wählen wir und behalten jede Zeile der Matrix als Array von Ganzzahlen von bis , wobei die te Zahl in ist Das Array entspricht den Bits der Zeile im Bereich von einschließlich bis exklusiv inO(n3/logn)wblog2nnb02b1iibmin(n,(i+1)b)0-Indexation. Wir können die Skalarprodukte von jeweils zwei solchen Blöcken in -Zeit vorberechnen . Das Aktualisieren einer Position in der Matrix ist schnell, da wir nur eine Ganzzahl ändern. Um das Skalarprodukt der Zeilen und iterieren Sie einfach über Arrays, die diesen Zeilen entsprechen. Suchen Sie nach Skalarprodukten der entsprechenden Blöcke in der Tabelle und fassen Sie die erhaltenen Produkte zusammen.O(22bb)ij

Der obige Absatz nimmt an, dass Operationen mit ganzen Zahlen nehmen Zeit. Dies ist eine weit verbreitete Annahme , da sie die Vergleichsgeschwindigkeit der Algorithmen normalerweise nicht ändert (wenn wir diese Annahme beispielsweise nicht verwenden, funktioniert die Brute-Force-Methode tatsächlich in der Zeit (hier) Wir messen die Zeit in Bitoperationen), wenn ganzzahlige Werte mit absoluten Werten von mindestens bis zu für eine Konstante annimmt (und wir das Problem ansonsten mit lösen können) Matrixmultiplikationen ohnehin), jedoch verwendet die oben vorgeschlagene Vier-Russen-MethodenO(1)O(n3logn)aijnεε>0O(nε)O(n3/logn) Operationen mit Nummern der Größe in diesem Fall; es werden also -Bitoperationen durchgeführt, die trotz der Änderung des Modells immer noch besser sind als Brute Force.O(logn)O(n3)

Die Frage nach der Existenz des -Ansatzes ist jedoch immer noch interessant.O(n3ε)

Die in dieser Antwort vorgestellten Techniken (Bitoptimierungen und Vier-Russen-Methode) sind keineswegs originell und werden hier der Vollständigkeit der Darstellung halber vorgestellt. Es war jedoch nicht trivial, einen Weg zu finden, sie anzuwenden.

Kaban-5
quelle
Erstens scheint Ihr Vorschlag in der Tat in der Praxis hilfreich zu sein. Ich könnte ihn in meinem Anwendungsfall ausprobieren. Vielen Dank! Zweitens ist die Rechenkomplexität Ihrer Algorithmen für jeden numerischen Typ mit fester Breite immer noch . Könnten Sie den -Ansatz näher erläutern ? Ich verstehe nicht, wie wir das Skalarprodukt von und schneller als finden könnten (was erforderlich wäre, wenn wir auf alle ihre Elemente zugreifen würden). O(n3)O(n3/logn)mat[i]mat[j]O(n)
user89217
Außerdem definiert Ihr Code nicht, matwas wichtig zu sein scheint. Ich verstehe, wie es definiert werden könnte, aber ich frage mich, ob (mat[i] & mat[j]).count()es mit jedem STL-Container wie gewünscht funktionieren würde.
user89217
1
In Bezug auf mat- ich denke, wir müssen verwenden std::vector<boost::dynamic_bitset<int64_t>>.
user89217
Zu mat: Ja, ich hatte eigentlich ein Standard-Bitset im Sinn, aber boost::dynamic_bitsetin diesem Fall ist es sogar noch besser, da seine Größe nicht konstant zur Kompilierungszeit sein muss. Wird die Antwort bearbeiten, um dieses Detail hinzuzufügen und den Ansatz der vier Russen zu verdeutlichen.
Kaban-5
1
Großartig, das sieht für mich solide aus. Ein kleiner Punkt: Da das transdichotome Modell davon ausgeht, dass wir Operationen mit Maschinenwörtern in ausführen können, müssen keine skalaren Produkte vorberechnet werden. Tatsächlich geht das Modell davon aus, dass , also ist mindestens so gut wie . Und wie Sie sagen, macht die Vorberechnung von Skalarprodukten keinen praktischen Sinn (eine Array-Suche ist langsamer als die binäre Operation). O(1)wlog2nO(n3/w)O(n3/logn)
user89217