Method.
Uses a constraint library to generate all event orderings satisfying the event constraints, without regard to (un)evenness, then finds the constrained solution with minimal unevenness iteratively.
Evenness ?
If unevenness is defined by looking at all the differences between days and event_days calculated and returning the max minus the min days difference then a need for clarification with your answer of:
1 2 3 4 5 6 7
a b c
Is that it has the values off to the left w.r.t. the days.
A better answer might be:
1 2 3 4 5 6 7
a b c
With the one shift to the right, their is less of a stretch from day 7 to any event day.
If you think the above is equivalent then you need to better define evenness - is it evennness over the extent of the event days perhaps?
STOP PRESS! Evenness has been edited by author and is now better described.
Code
The source is set to run your example if you just hit return on each prompt.
# -*- coding: utf-8 -*-
"""
Even distribution of directed acyclic graph with respect to edge length
https://stackoverflow.com/questions/71532362/even-distribution-of-directed-acyclic-graph-with-respect-to-edge-length
Created on Sat Mar 26 08:53:19 2022
@author: paddy
"""
# https://pypi.org/project/python-constraint/
from constraint import Problem
from itertools import product
#%% Inputs
days = input("Days: int = ")
try:
days = int(days.strip())
except:
days = 7
print(f"Using {days=}")
events = input("events: space_separated = ")
events = events.strip().split()
if not events:
events = list("abc")
print(f"Using {events=}")
constraint_funcs = []
while True:
constr = input("constraint: string_expression (. to end) = ").strip()
if not constr:
constraint_funcs = ["1 <= (b - a) <=2",
"(c - a) <= 4",
"(c - b) >= 3"]
break
if constr == '.':
break
constraint_funcs.append(constr)
print(f"\nUsing {constraint_funcs=}")
#%% Constraint Setup
print()
problem = Problem()
problem.addVariables(events, range(1, days+1))
for constr in constraint_funcs:
constr_events = sorted( set(events) & set(compile(constr, '<input>',
mode='eval').co_names))
expr = f"lambda {', '.join(constr_events)}: {constr}"
print(f" Add Constraint {expr!r}, On {constr_events}")
func = eval(expr)
problem.addConstraint(func, constr_events)
#%% Solution optimisation for "evenness"
print()
def unevenness(event_days: list[int], all_days: int):
"(Max - min diff between ordered events, leftmost event)"
maxdiff, mindiff = -1, all_days + 1
for event, next_event in zip(event_days, event_days[1:]):
diff = next_event - event
maxdiff = max(maxdiff, diff)
mindiff = min(mindiff, diff)
return maxdiff - mindiff, event_days[0]
def printit(solution, all_days):
drange = range(1, all_days+1)
# print(solution)
print(' '.join(str(i)[0] for i in drange))
for event, day in sorted(solution.items(), key=lambda kv: kv[::-1]):
print(' ' * (day - 1) + event )
print()
current_best = None, 9e99
for ans in problem.getSolutionIter():
unev = unevenness(sorted(ans.values()), days)
if current_best[0] is None or unev < current_best[1]:
current_best = ans, unev
print("Best so far:")
printit(ans, days)
The unevenness function has been updated. A better function might be to minimise the standard deviations of the days between successive events, but for this example, this works.
Output
Sample run using your constraints, but longer event names.
Days: int = 7
Using days=7
events: space_separated = arch belt card
Using events=['arch', 'belt', 'card']
constraint: string_expression (. to end) = 1 <= (belt - arch) <= 2
constraint: string_expression (. to end) = (card - arch) <= 4
constraint: string_expression (. to end) = (card - belt) >= 3
constraint: string_expression (. to end) = .
Using constraint_funcs=['1 <= (belt - arch) <= 2', '(card - arch) <= 4', '(card - belt) >= 3']
Add Constraint 'lambda arch, belt: 1 <= (belt - arch) <= 2', On ['arch', 'belt']
Add Constraint 'lambda arch, card: (card - arch) <= 4', On ['arch', 'card']
Add Constraint 'lambda belt, card: (card - belt) >= 3', On ['belt', 'card']
Best so far:
1 2 3 4 5 6 7
arch
belt
card
Best so far:
1 2 3 4 5 6 7
arch
belt
card
Best so far:
1 2 3 4 5 6 7
arch
belt
card
Note: The first letter of event names align with the day columns.
The second item in the return tuple of the unevenness function is the day that the earliest, (left-most), event happens. if the spread of the events are equal, this will tend to favour the solution further to the left when minimised.