Given a graph \$G(V, E)\$ with vertices \$V\$ and edges \$E\$, where each vertex is associated with a single color from a set of colors \$C=\{1, 2, ..., k\}\$, we define the following problem:
Problem Statement:
- Input: A graph \$G(V, E)\$ and a set of colors \$C\$, with a constraint that each vertex \$v\$ in \$V\$ is assigned exactly one color from \$C\$.
- Objective: Determine if there exists a connected subgraph of \$G\$ that is "\$C\$-colorful". A subgraph is considered "\$C\$-colorful" if it contains exactly one vertex of each color present in \$C\$.
Background:
Topology-Free Querying of Protein Interaction Networks SHARON BRUCKNER et al. (2010, Journal of Computational biology)
Complexity:
The problem has been proven to be NP-complete, the authors say there is a solution using Dynamic Programming that is \$\mathcal{O}(3^k \times E)\$.
Example Inputs:
example_input = {
"vertices": ["v1", "v2", "v3", "v4", "v5"],
"edges": [
("v1", "v2"),
("v2", "v3"),
("v3", "v4"),
("v4", "v5"),
("v1", "v3"),
("v2", "v4"),
],
"color_constraints": {"v1": 1, "v2": 2, "v3": 3, "v4": 1, "v5": 2},
"num_colors": 3,
} # Should be True
example_input_2 = {
"vertices": ["v1", "v2", "v3", "v4", "v5"],
"edges": [
("v1", "v2"),
("v2", "v3"),
("v3", "v4"),
("v4", "v5"),
("v1", "v3"),
("v2", "v4"),
],
"color_constraints": {"v1": 2, "v2": 1, "v3": 1, "v4": 1, "v5": 3},
"num_colors": 3,
} # Should be False
example_input_3 = {
"vertices": ["v1", "v2", "v3", "v4", "v5"],
"edges": [
("v1", "v2"),
("v2", "v3"),
("v3", "v4"),
("v4", "v5"),
("v1", "v3"),
("v2", "v4"),
],
"color_constraints": {"v1": 2, "v2": 1, "v3": 1, "v4": 2, "v5": 3},
"num_colors": 3,
} # should be True
My implementation of the authors' DP solution
from itertools import combinations
def powerset(iterable):
s = list(iterable)
list_all_combinations = []
for r in range(len(s)+1):
list_all_combinations += list(combinations(s, r))
return list_all_combinations
# powerset([1, 2, 3]) -> [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
def find_colorful_subtree(graph_info) -> bool:
"""
graph_info is a dictionary which keys are 'vertices', 'edges', 'color_constraints', and 'num_colors' and values are
dictionaries.
"""
vertices: list[str]
edges: list[tuple[str, str]]
vertices, edges = graph_info['vertices'], graph_info['edges']
v_to_idx: dict[str, int] = {v: idx for idx, v in enumerate(vertices)} # idx_to_v is not needed, it's vertices
color_map: dict[str, int] = graph_info['color_constraints']
num_colors: int = graph_info['num_colors']
# Create graph
g = {v: set() for v in vertices}
for u, v in edges:
g[u].add(v)
g[v].add(u)
# DP table dp[v][S] = True if a subtree rooted at v is S-colorful
# We encode S with integers (bitmask)
dp = [[False for _ in range(1 << num_colors)] for _ in range(len(vertices))]
# Initialize the DP table
for v_idx, v in enumerate(vertices):
color = color_map[v]
dp[v_idx][1 << color - 1] = True
# Recursive function to fill the DP table
for subset in powerset(range(num_colors)):
if len(subset) <= 1:
continue
# generate all s1, s2 such that s1 + s2 = subset and s1 & s2 = {}
s: int = 0
for color in subset:
s |= 1 << color
for left_subset in powerset(subset):
if len(left_subset) == 0 or len(left_subset) == len(subset):
continue
left_mask: int = 0
for color in left_subset:
left_mask |= 1 << color
right_mask: int = s ^ left_mask # ^ -> XOR
for v_idx, v in enumerate(vertices):
if (1 << color_map[v] - 1) & left_mask != 0:
for u in g[v]:
if (1 << color_map[u] - 1) & right_mask == 0:
continue
if dp[v_idx][left_mask] and dp[v_to_idx[u]][right_mask]:
dp[v_idx][s] = True
break # no need to check other neighbors, we found a colorful subtree rooted at v
ans: bool = False
for v_idx, v in enumerate(vertices):
ans |= dp[v_idx][(1 << num_colors) - 1]
return ans
# Running the algorithm on the examples
assert find_colorful_subtree(example_input)
assert not find_colorful_subtree(example_input_2)
assert find_colorful_subtree(example_input_3)
A naive implementation in complexity \$\mathcal{O}(V 2^V)\$.
def find_colorful_subtree_naive(graph_info) -> bool:
"""
graph_info is a dictionary which keys are 'vertices', 'edges', 'color_constraints', and 'num_colors' and values are
dictionaries.
"""
vertices: list[str]
edges: list[tuple[str, str]]
vertices, edges = graph_info['vertices'], graph_info['edges']
v_to_idx: dict[str, int] = {v: idx for idx, v in enumerate(vertices)} # idx_to_v is not needed, it's vertices
color_map: dict[str, int] = graph_info['color_constraints']
num_colors: int = graph_info['num_colors']
# Create graph
g = {v: set() for v in vertices}
for u, v in edges:
g[u].add(v)
g[v].add(u)
# we take a set of vertices (2^(|V|)) and check if it is colorful (O(|V|))
for s in range(1 << len(vertices)):
if bin(s).count('1') < num_colors:
continue
# Check if the set is colorful
colors: set[int] = set()
for v_idx, v in enumerate(vertices):
if (1 << v_idx) & s:
if color_map[v] in colors:
break
colors.add(color_map[v])
if len(colors) == num_colors:
# Check if the set of vertices is connected
visited: set[str] = set()
def dfs(v: str):
visited.add(v)
for u in g[v]:
if u not in visited and (1 << v_to_idx[u]) & s:
dfs(u)
for v_idx, v in enumerate(vertices):
if (1 << v_idx) & s:
dfs(v)
break
if len(visited) == bin(s).count('1'):
return True
return False
# Running the algorithm on the examples
assert find_colorful_subtree_naive(example_input)
assert not find_colorful_subtree_naive(example_input_2)
assert find_colorful_subtree_naive(example_input_3)
Are there things I could improve for my implementations? Are there things I got wrong?