Skip to content

Commit

Permalink
Merge pull request #35 from lzear/fix-ranked-pairs
Browse files Browse the repository at this point in the history
fix(ranked pairs): implement Tarjan algorithm
  • Loading branch information
lzear authored Dec 21, 2020
2 parents 82292cf + 0126633 commit e117481
Show file tree
Hide file tree
Showing 5 changed files with 172 additions and 99 deletions.
62 changes: 62 additions & 0 deletions src/methods/ranked-pairs/Tarjan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// From chadhutchins:
// https://gist.github.com/chadhutchins/1440602

import { Vertex } from './Vertex'
import { VertexStack } from './VertexStack'

export class Tarjan {
private index: number
private stack: VertexStack
private readonly graph: Vertex[]
// scc: Vertex[][]

constructor(graph: Vertex[]) {
this.index = 0
this.stack = new VertexStack()
this.graph = graph
// this.scc = []
}
public run(): void {
for (const i in this.graph)
if (this.graph[i].index < 0) this.strongconnect(this.graph[i])
// return this.scc
}
private strongconnect(vertex: Vertex): void {
// Set the depth index for v to the smallest unused index
vertex.index = this.index
vertex.lowlink = this.index
this.index = this.index + 1
this.stack.vertices.push(vertex)

// Consider successors of v
// aka... consider each vertex in vertex.connections
for (const i in vertex.connections) {
const v = vertex
const w = vertex.connections[i]
if (w.index < 0) {
// Successor w has not yet been visited; recurse on it
this.strongconnect(w)
v.lowlink = Math.min(v.lowlink, w.lowlink)
} else if (this.stack.contains(w)) {
// Successor w is in stack S and hence in the current SCC
v.lowlink = Math.min(v.lowlink, w.index)
}
}

// If v is a root node, pop the stack and generate an SCC
if (vertex.lowlink == vertex.index) {
// start a new strongly connected component
const vertices: Vertex[] = []
let w = null
if (this.stack.vertices.length > 0)
do {
w = this.stack.vertices.pop()
// add w to current strongly connected component
w && vertices.push(w)
} while (w && !vertex.equals(w))
// output the current strongly connected component
// ... i'm going to push the results to a member scc array variable
// if (vertices.length > 0) this.scc.push(vertices)
}
}
}
27 changes: 27 additions & 0 deletions src/methods/ranked-pairs/Vertex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// From chadhutchins:
// https://gist.github.com/chadhutchins/1440602

export class Vertex {
public index: number
public lowlink: number
public connections: Vertex[]
private readonly name: number | string
constructor(name: number | string) {
this.connections = []
this.name = name

// used in tarjan algorithm
// went ahead and explicity initalized them
this.index = -1
this.lowlink = -1
}

public equals(vertex: Vertex): boolean {
// equality check based on vertex name
return this.name == vertex.name
}

public connect(vertex: Vertex): void {
this.connections.push(vertex)
}
}
18 changes: 18 additions & 0 deletions src/methods/ranked-pairs/VertexStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
// From chadhutchins:
// https://gist.github.com/chadhutchins/1440602

import { Vertex } from './Vertex'

export class VertexStack {
public vertices: Vertex[]

constructor() {
this.vertices = []
}

public contains(vertex: Vertex): boolean {
for (const i in this.vertices)
if (this.vertices[i].equals(vertex)) return true
return false
}
}
126 changes: 27 additions & 99 deletions src/methods/ranked-pairs/index.ts
Original file line number Diff line number Diff line change
@@ -1,98 +1,34 @@
import concat from 'lodash/concat'
import flatten from 'lodash/flatten'
import groupBy from 'lodash/groupBy'
import range from 'lodash/range'
import without from 'lodash/without'
import uniq from 'lodash/uniq'
import zipObject from 'lodash/zipObject'
import {
Matrix,
ScoreObject,
SystemUsingMatrix,
VotingSystem,
} from '../../types'
import { Tarjan } from './Tarjan'
import { Vertex } from './Vertex'

type Edge = { from: number; to: number; value: number }

const findGraphRoots = (graph: Edge[]) => {
const { sources } = graph.reduce(
({ sources, targets }, { from, to }) => ({
sources: concat(
without(sources, to),
targets.includes(from) || sources.includes(from) ? [] : [from],
),
targets: targets.includes(to) ? targets : [...targets, to],
}),
{ sources: [] as number[], targets: [] as number[] },
const generateAcyclicGraph = (graph: Edge[], edgesToAdd: Edge[]): Edge[] => {
const allEdges = [...graph, ...edgesToAdd]
const vDict = {} as { [i: number]: Vertex }
uniq(allEdges.flatMap((e) => [e.from, e.to])).forEach(
(c) => (vDict[c] = new Vertex(c)),
)
return sources
allEdges.forEach((e) => vDict[e.from].connect(vDict[e.to]))
const tarjan = new Tarjan(Object.values(vDict))
tarjan.run()
return [
...graph,
...edgesToAdd.filter(
(edge) => vDict[edge.from].lowlink !== vDict[edge.to].lowlink,
),
]
}

const canAddEdgeToAcyclicGraph = (graph: Edge[], toAdd: Edge) => {
const active: number[] = []
const visited: number[] = []
let cur: number | undefined = toAdd.to
while (cur !== undefined) {
visited.push(cur)
for (const { to } of graph.filter(({ from }) => from === cur)) {
if (to === toAdd.from) return false
if (!visited.includes(to) && !active.includes(to)) active.push(to)
}
cur = active.pop()
}
return true
}

const graphSignature = (graph: Edge[]) =>
graph
.sort((a, b) => b.from - a.from || b.to - a.to)
.map((edge) => `${edge.from}-${edge.to}`)
.join(';')

const dedupe = (graphs: Edge[][]) =>
graphs.reduce(
(acc, graph) => {
const signature = graphSignature(graph)
if (acc.signatures.includes(signature)) return acc
return {
deduped: [...acc.deduped, graph],
signatures: [...acc.signatures, signature],
}
},
{ deduped: [] as Edge[][], signatures: [] as string[] },
).deduped

const generateAcyclicGraphs = (graph: Edge[], edgesToAdd: Edge[]): Edge[][] => {
const validEdgesToAdd = edgesToAdd.filter((toAdd) =>
canAddEdgeToAcyclicGraph(graph, toAdd),
)
return validEdgesToAdd.length
? dedupe(
validEdgesToAdd.flatMap((toAdd, k) =>
generateAcyclicGraphs(
[...graph, toAdd],
validEdgesToAdd.filter((_, kk) => kk !== k),
),
),
)
: [graph]
}

const sortTopEdges = (group: Edge[]): Edge[][] => {
const sources = findGraphRoots(group)
if (sources.length) {
const { bot, top } = group.reduce(
({ top, bot }, cur) =>
sources.includes(cur.from)
? { bot, top: [...top, cur] }
: { bot: [...bot, cur], top },
{ bot: [] as Edge[], top: [] as Edge[] },
)
if (top.length > 0 && top.length < group.length)
return [top, ...sortTopEdges(bot)]
}
return [group]
}

export const rankedPairs: SystemUsingMatrix = {
type: VotingSystem.RankedPairs,
computeFromMatrix(matrix: Matrix): ScoreObject {
Expand All @@ -107,42 +43,34 @@ export const rankedPairs: SystemUsingMatrix = {
const edgesGroups = groupBy(allEdges, 'value')
const groups = Object.keys(edgesGroups)
.sort((a, b) => Number(b) - Number(a))
.flatMap((value) => sortTopEdges(edgesGroups[value]))
const acyclicGraphs = groups.reduce(
(graphs: Edge[][], edgesToAdd) =>
graphs.flatMap((graph) => generateAcyclicGraphs(graph, edgesToAdd)),
[[]] as Edge[][],
.map((value) => edgesGroups[value])

const acyclicGraph = groups.reduce(
(graph: Edge[], edgesToAdd) => generateAcyclicGraph(graph, edgesToAdd),
[] as Edge[],
)
const graphsWinners = acyclicGraphs.map((acyclicGraph) =>
range(matrix.candidates.length).filter(
(candidate, key) => !acyclicGraph.some(({ to }) => to === key),
),
const graphsWinners = range(matrix.candidates.length).filter(
(candidate, key) => !acyclicGraph.some(({ to }) => to === key),
)

const scores = flatten(graphsWinners).reduce((acc, curr) => {
const scores = graphsWinners.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1
return acc
}, {} as { [k: number]: number })

const maxScore1 = Math.max(...Object.values(scores))

const winnersIdx = range(matrix.candidates.length).filter(
(i) => scores[i] === maxScore1,
)

if (winnersIdx.length === matrix.candidates.length)
return zipObject(
matrix.candidates,
Array(matrix.candidates.length).fill(1),
)
const nextRound = {
const nextResults = rankedPairs.computeFromMatrix({
array: matrix.array
.filter((c, k) => !winnersIdx.includes(k))
.map((row) => row.filter((c, k) => !winnersIdx.includes(k))),
candidates: matrix.candidates.filter((c, k) => !winnersIdx.includes(k)),
}

const nextResults = rankedPairs.computeFromMatrix(nextRound)
})
const maxScore2 = Math.max(...Object.values(nextResults))
return winnersIdx.reduce(
(scoreObject, winnerIdx) => ({
Expand Down
38 changes: 38 additions & 0 deletions src/methods/ranked-pairs/rankedPairs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,4 +62,42 @@ describe('ranked pairs', () => {
e: 3,
})
})
it('completes computation in decent time', () => {
expect(
rankedPairs.computeFromMatrix({
array: [
[0, 1, -1, -1, -1, -1, 1, -1, 1],
[-1, 0, -1, 1, -1, -1, -1, 1, -1],
[1, 1, 0, 1, -1, 0, 1, -1, 0],
[1, -1, -1, 0, -1, -1, 0, -1, 0],
[1, 1, 1, 1, 0, -1, 1, -1, 1],
[1, 1, 0, 1, 1, 0, 0, -1, 1],
[-1, 1, -1, 0, -1, 0, 0, -1, -1],
[1, -1, 1, 1, 1, 1, 1, 0, 1],
[-1, 1, 0, 0, -1, -1, 1, -1, 0],
],
candidates: [
'bwLvxwn4',
'Bi8rD2kq',
'XuHBc1ME',
'xhAvdxz2',
'MBDuJLcU',
'4lNHn78L',
'hNtQKVPG',
'KXxHiFYK',
'2PWfQstO',
],
}),
).toEqual({
'2PWfQstO': 3,
'4lNHn78L': 4,
Bi8rD2kq: 1,
KXxHiFYK: 4,
MBDuJLcU: 4,
XuHBc1ME: 4,
bwLvxwn4: 4,
hNtQKVPG: 2,
xhAvdxz2: 4,
})
})
})

0 comments on commit e117481

Please sign in to comment.