Coverage Report

Created: 2026-08-13 06:35

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/igraph/src/centrality/truss.cpp
Line
Count
Source
1
/*
2
  Copyright 2017 The Johns Hopkins University Applied Physics Laboratory LLC. All Rights Reserved.
3
  Copyright 2021 The igraph team.
4
5
  Truss algorithm for cohesive subgroups.
6
7
  Author: Alex Perrone
8
  Date: 2017-08-03
9
  Minor edits: The igraph team, 2021
10
11
  This program is free software; you can redistribute it and/or modify
12
  it under the terms of the GNU General Public License as published by
13
  the Free Software Foundation; either version 2 of the License, or
14
  (at your option) any later version.
15
16
  This program is distributed in the hope that it will be useful,
17
  but WITHOUT ANY WARRANTY; without even the implied warranty of
18
  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
  GNU General Public License for more details.
20
21
  You should have received a copy of the GNU General Public License
22
  along with this program; if not, write to the Free Software
23
  Foundation, Inc.,  51 Franklin Street, Fifth Floor, Boston, MA
24
  02110-1301 USA
25
26
*/
27
28
#include "igraph_community.h"
29
30
#include "igraph_adjlist.h"
31
#include "igraph_error.h"
32
#include "igraph_interface.h"
33
#include "igraph_motifs.h"
34
#include "igraph_structural.h"
35
36
#include "core/exceptions.h"
37
#include "core/interruption.h"
38
39
#include <vector>
40
#include <unordered_set>
41
42
using std::vector;
43
using std::unordered_set;
44
45
46
// Unpack the triangles as a vector of vertices to be a vector of edges.
47
// So, instead of the triangle specified as vertices [1, 2, 3], return the
48
// edges as [1, 2, 1, 3, 2, 3] so that the support can be computed.
49
1.31k
static igraph_error_t igraph_truss_i_unpack(const igraph_vector_int_t *tri, igraph_vector_int_t *unpacked_tri) {
50
1.31k
    igraph_int_t num_triangles = igraph_vector_int_size(tri);
51
52
1.31k
    IGRAPH_CHECK(igraph_vector_int_resize(unpacked_tri, 2 * num_triangles));
53
54
23.7k
    for (igraph_int_t i = 0, j = 0; i < num_triangles; i += 3, j += 6) {
55
22.4k
        VECTOR(*unpacked_tri)[j]   = VECTOR(*unpacked_tri)[j+2] = VECTOR(*tri)[i];
56
22.4k
        VECTOR(*unpacked_tri)[j+1] = VECTOR(*unpacked_tri)[j+4] = VECTOR(*tri)[i+1];
57
22.4k
        VECTOR(*unpacked_tri)[j+3] = VECTOR(*unpacked_tri)[j+5] = VECTOR(*tri)[i+2];
58
22.4k
    }
59
60
1.31k
    return IGRAPH_SUCCESS;
61
1.31k
}
62
63
64
// Compute the edge support, i.e. number of triangles each edge occurs in.
65
// Time complexity: O(m), where m is the number of edges listed in eid.
66
1.31k
static void igraph_truss_i_compute_support(const igraph_vector_int_t *eid, igraph_vector_int_t *support) {
67
1.31k
    igraph_int_t m = igraph_vector_int_size(eid);
68
68.5k
    for (igraph_int_t i = 0; i < m; ++i) {
69
67.2k
        VECTOR(*support)[VECTOR(*eid)[i]] += 1;
70
67.2k
    }
71
1.31k
}
72
73
74
/* internal function doing the computations once the support is defined */
75
static igraph_error_t igraph_i_trussness(const igraph_t *graph, igraph_vector_int_t *support,
76
1.31k
                                         igraph_vector_int_t *trussness) {
77
1.31k
    IGRAPH_HANDLE_EXCEPTIONS_BEGIN;
78
79
1.31k
    igraph_adjlist_t adjlist;
80
1.31k
    igraph_vector_int_t commonNeighbors;
81
1.31k
    igraph_vector_bool_t completed;
82
83
    // C++ data structures
84
1.31k
    vector< unordered_set<igraph_int_t> > vec;
85
86
    // Allocate memory for result
87
1.31k
    igraph_int_t no_of_edges = igraph_vector_int_size(support);
88
1.31k
    IGRAPH_CHECK(igraph_vector_int_resize(trussness, no_of_edges));
89
1.31k
    if (no_of_edges == 0) {
90
45
        return IGRAPH_SUCCESS;
91
45
    }
92
93
    // Get max possible value = max entry in support.
94
    // This cannot be computed if there are no edges, hence the above check
95
1.27k
    igraph_int_t max = igraph_vector_int_max(support);
96
97
    // Initialize completed edges.
98
1.27k
    IGRAPH_VECTOR_BOOL_INIT_FINALLY(&completed, no_of_edges);
99
100
    // The vector of levels. Each level of the vector is a set of edges initially
101
    // at that level of support, where support is # of triangles the edge is in.
102
1.27k
    vec.resize(max + 1);
103
104
    // Add each edge to its appropriate level of support.
105
31.3k
    for (igraph_int_t i = 0; i < no_of_edges; ++i) {
106
30.0k
        vec[VECTOR(*support)[i]].insert(i);  // insert edge i into its support level
107
30.0k
    }
108
109
    // Record the trussness of edges at level 0. These edges are not part
110
    // of any triangles, so there's not much to do and we "complete" them
111
10.9k
    for (auto edge : vec[0]) {
112
10.9k
        VECTOR(*trussness)[edge] = 2;
113
10.9k
        VECTOR(completed)[edge] = true;
114
10.9k
    }
115
116
    // Initialize variables needed below.
117
1.27k
    IGRAPH_CHECK(igraph_adjlist_init(graph, &adjlist, IGRAPH_ALL, IGRAPH_NO_LOOPS, IGRAPH_MULTIPLE));
118
1.27k
    IGRAPH_FINALLY(igraph_adjlist_destroy, &adjlist);
119
1.27k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&commonNeighbors, 0);
120
121
    // Move through the levels, one level at a time, starting at first level.
122
5.19k
    for (igraph_int_t level = 1; level <= max; ++level) {
123
124
        /* Track down edges one at a time */
125
23.1k
        while (!vec[level].empty()) {
126
19.1k
            IGRAPH_ALLOW_INTERRUPTION();
127
128
19.1k
            igraph_int_t seed = *vec[level].begin();  // pull out the first edge
129
19.1k
            vec[level].erase(seed);  // remove the first element
130
131
            /* Find the vertices of this edge */
132
19.1k
            igraph_int_t fromVertex = IGRAPH_FROM(graph, seed);
133
19.1k
            igraph_int_t toVertex = IGRAPH_TO(graph, seed);
134
135
            /* Find neighbors of both vertices. If they run into each other,
136
             * there is a triangle. We rely on the neighbor lists being sorted,
137
             * as guaranteed by igraph_adjlist_init(), when computing intersections. */
138
19.1k
            igraph_vector_int_t *fromNeighbors = igraph_adjlist_get(&adjlist, fromVertex);
139
19.1k
            igraph_vector_int_t *toNeighbors = igraph_adjlist_get(&adjlist, toVertex);
140
19.1k
            igraph_vector_int_t *q1 = fromNeighbors;
141
19.1k
            igraph_vector_int_t *q2 = toNeighbors;
142
143
19.1k
            if (igraph_vector_int_size(q1) > igraph_vector_int_size(q2)) {
144
                // case: #fromNeighbors > #toNeigbors, so make q1 the smaller set.
145
5.57k
                q1 = toNeighbors;
146
5.57k
                q2 = fromNeighbors;
147
5.57k
            }
148
149
            // Intersect the neighbors.
150
19.1k
            IGRAPH_CHECK(igraph_vector_int_intersect_sorted(q1, q2, &commonNeighbors));
151
152
            /* Go over the overlapping neighbors and check each */
153
19.1k
            igraph_int_t ncommon = igraph_vector_int_size(&commonNeighbors);
154
86.4k
            for (igraph_int_t j = 0; j < ncommon; j++) {
155
67.2k
                igraph_int_t n = VECTOR(commonNeighbors)[j];  // the common neighbor
156
67.2k
                igraph_int_t e1, e2;
157
158
67.2k
                igraph_get_eid(graph, &e1, fromVertex, n, IGRAPH_UNDIRECTED, /* error= */ false);
159
67.2k
                igraph_get_eid(graph, &e2, toVertex, n, IGRAPH_UNDIRECTED, /* error= */ false);
160
67.2k
                IGRAPH_ASSERT(e1 >= 0 && e2 >= 0);
161
162
67.2k
                bool e1_complete = VECTOR(completed)[e1];
163
67.2k
                bool e2_complete = VECTOR(completed)[e2];
164
165
67.2k
                if (!e1_complete && !e2_complete) {
166
22.4k
                    igraph_int_t newLevel;
167
168
                    // Demote this edge, if higher than current level.
169
22.4k
                    if (VECTOR(*support)[e1] > level) {
170
9.11k
                        VECTOR(*support)[e1] -= 1;  // decrement the level
171
9.11k
                        newLevel = VECTOR(*support)[e1];
172
9.11k
                        vec[newLevel].insert(e1);
173
9.11k
                        vec[newLevel + 1].erase(e1);  // the old level
174
9.11k
                    }
175
                    // Demote this edge, if higher than current level.
176
22.4k
                    if (VECTOR(*support)[e2] > level) {
177
10.9k
                        VECTOR(*support)[e2] -= 1;  // decrement the level
178
10.9k
                        newLevel = VECTOR(*support)[e2];
179
10.9k
                        vec[newLevel].insert(e2);
180
10.9k
                        vec[newLevel + 1].erase(e2);  // the old level
181
10.9k
                    }
182
22.4k
                }
183
67.2k
            }
184
            // Record this edge; its level is its trussness.
185
19.1k
            VECTOR(*trussness)[seed] = level + 2;
186
19.1k
            VECTOR(completed)[seed] = true; // mark as complete
187
19.1k
            igraph_vector_int_clear(&commonNeighbors);
188
19.1k
        }  // end while
189
3.92k
    }  // end for-loop over levels
190
191
    // Clean up.
192
1.27k
    igraph_vector_int_destroy(&commonNeighbors);
193
1.27k
    igraph_adjlist_destroy(&adjlist);
194
1.27k
    igraph_vector_bool_destroy(&completed);
195
1.27k
    IGRAPH_FINALLY_CLEAN(3);
196
197
1.27k
    return IGRAPH_SUCCESS;
198
199
1.27k
    IGRAPH_HANDLE_EXCEPTIONS_END;
200
0
}
201
202
203
/**
204
 * \function igraph_trussness
205
 * \brief Finding the "trussness" of the edges in a network.
206
 *
207
 * A k-truss is a subgraph in which every edge occurs in at least <code>k-2</code> triangles
208
 * in the subgraph. The trussness of an edge indicates the highest k-truss that
209
 * the edge occurs in.
210
 *
211
 * </para><para>
212
 * This function returns the highest \c k for each edge. If you are interested in
213
 * a particular k-truss subgraph, you can subset the graph to those edges
214
 * which are <code>&gt;= k</code> because each k-truss is a subgraph of a <code>(k–1)</code>-truss
215
 * Thus, to get all 4-trusses, take <code>k >= 4</code> because the 5-trusses, 6-trusses,
216
 * etc. need to be included.
217
 *
218
 * </para><para>
219
 * The current implementation of this function iteratively decrements support
220
 * of each edge using O(|E|) space and O(|E|^1.5) time. The implementation does
221
 * not support multigraphs; use \ref igraph_simplify() to collapse edges before
222
 * calling this function.
223
 *
224
 * </para><para>
225
 * Reference:
226
 *
227
 * </para><para>
228
 * See Algorithm 2 in:
229
 * Wang, Jia, and James Cheng. "Truss decomposition in massive networks."
230
 * Proceedings of the VLDB Endowment 5.9 (2012): 812-823.
231
 * https://doi.org/10.14778/2311906.2311909
232
 *
233
 * \param graph The input graph. Loop edges are allowed; multigraphs are not.
234
 * \param truss Pointer to initialized vector of truss values that will
235
 * indicate the highest k-truss each edge occurs in. It will be resized as
236
 * needed.
237
 * \return Error code.
238
 *
239
 * Time complexity: It should be O(|E|^1.5) according to the reference.
240
 */
241
1.31k
igraph_error_t igraph_trussness(const igraph_t* graph, igraph_vector_int_t* trussness) {
242
1.31k
    igraph_vector_int_t triangles, support, unpacked_triangles, eid;
243
1.31k
    igraph_bool_t is_multigraph;
244
245
    /* Check whether the graph is a multigraph; trussness will not work for these */
246
1.31k
    IGRAPH_CHECK(igraph_has_multiple(graph, &is_multigraph));
247
1.31k
    if (! is_multigraph && igraph_is_directed(graph)) {
248
        /* Directed graphs with mutual edges are effectively multigraphs
249
         * when edge directions are ignored. */
250
0
        IGRAPH_CHECK(igraph_has_mutual(graph, &is_multigraph, /* loops */ false));
251
0
    }
252
1.31k
    if (is_multigraph) {
253
0
        IGRAPH_ERROR("Trussness is not implemented for graphs with multi-edges.", IGRAPH_UNIMPLEMENTED);
254
0
    }
255
256
    /* Manage the stack to make it memory safe: do not change the order of
257
     * initialization of the following four vectors */
258
1.31k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&support, igraph_ecount(graph));
259
1.31k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&eid, 0);
260
1.31k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&unpacked_triangles, 0);
261
1.31k
    IGRAPH_VECTOR_INT_INIT_FINALLY(&triangles, 0);
262
263
    // List the triangles as vertex triplets.
264
1.31k
    IGRAPH_CHECK(igraph_list_triangles(graph, &triangles));
265
266
    // Unpack the triangles from vertex list to edge list.
267
1.31k
    IGRAPH_CHECK(igraph_truss_i_unpack(&triangles, &unpacked_triangles));
268
1.31k
    igraph_vector_int_destroy(&triangles);
269
1.31k
    IGRAPH_FINALLY_CLEAN(1);
270
271
    // Get the edge IDs of the unpacked triangles. Note: a given eid can occur
272
    // multiple times in this list if it is in multiple triangles.
273
1.31k
    IGRAPH_CHECK(igraph_get_eids(graph, &eid, &unpacked_triangles, /* directed = */ false, /* error = */ true));
274
1.31k
    igraph_vector_int_destroy(&unpacked_triangles);
275
1.31k
    IGRAPH_FINALLY_CLEAN(1);
276
277
    // Compute the support of the edges.
278
1.31k
    igraph_truss_i_compute_support(&eid, &support);
279
1.31k
    igraph_vector_int_destroy(&eid);
280
1.31k
    IGRAPH_FINALLY_CLEAN(1);
281
282
    // Compute the trussness of the edges.
283
1.31k
    IGRAPH_CHECK(igraph_i_trussness(graph, &support, trussness));
284
1.31k
    igraph_vector_int_destroy(&support);
285
1.31k
    IGRAPH_FINALLY_CLEAN(1);
286
287
1.31k
    return IGRAPH_SUCCESS;
288
1.31k
}