<jupyter_start><jupyter_text>Solution runner and writer.<jupyter_code>def mk_output_dir():
    now = datetime.now()
    current_time = now.strftime("%H%M%S")
    simple_name = SOLUTION_NAMES[len(os.listdir(ROOT_OUTPUT_PATH))]
    OUTPUT_PATH = os.path.join(ROOT_OUTPUT_PATH, f"{current_time}_{simple_name}_{abs(hash(datetime.now()))})")
    os.makedirs(OUTPUT_PATH, exist_ok=True)
    return OUTPUT_PATH

def write_bresult(s):
    with open(os.path.join(OUTPUT_PATH, "res.pkl"), "wb") as f:
        pickle.dump(s, f)

def load_bresult(s):
    with open(os.path.join(ROOT_OUTPUT_PATH, s, "res.pkl"), "rb") as f:
        return pickle.load(f)

def run(i):
    parsed_input = parse_input(os.path.join(INPUT_PATH, i))
    s = compute(parsed_input)
    v = score(parsed_input, s)
    print(f"%s: \t\t{v:,}" % (i.split("_")[0]))
    with open(os.path.join(OUTPUT_PATH, f"{i}_result"), "w") as f:
        f.writelines(map(lambda x: f"{x}\n", format_output(s)))
    return s, v

def new_sol(x=None):
    global OUTPUT_PATH
    OUTPUT_PATH = mk_output_dir()
    files = sorted([x for x in os.listdir(INPUT_PATH) if not x.startswith(".")])
    if x is not None:
        if isinstance(x, list):
            files = x
        else:
            files = [f for f in files if f.startswith(x)]
    print(files)
    # multiprocessing
    #pool = multiprocessing.Pool(processes=len(files))
    #res = pool.map(run, files)
    # sequential
    res = list(map(run, files))
    s, scores = list(map(itemgetter(0), res)), list(map(itemgetter(1), res))
    print(f"------------------------------------")
    print(f"Total: \t\t{sum(scores):,}")
    #pool.close()
    write_bresult(s)
    Javascript(save)
    copyfile(os.path.join(os.getcwd(), NB_NAME), os.path.join(OUTPUT_PATH, NB_NAME))
    return s<jupyter_output><empty_output><jupyter_text>--- Data structures* Meta contains file name and high level infos.* Contributor skills are stored in a dictionary (skill name: skill level) for efficient access.* Project requirements is a list of Skill objects* Schedule is a project name and a list of Contributor objects.<jupyter_code>Meta = namedtuple('Meta', 'file_name num_contributors num_projects')
Contributor = namedtuple('Contributor', 'id name num_skills skills')
Skill = namedtuple('Skill', 'name level')
Project = namedtuple('Project', 'id name num_days score best_before num_roles requirements')
Schedule = namedtuple('Schedule', 'name contributors')<jupyter_output><empty_output><jupyter_text>Input parsing and output formatting (not writing)<jupyter_code>def parse_skill(line):
    skill_raw = line.split()
    return Skill(skill_raw[0], int(skill_raw[1]))

def parse_input(fname):
    with open(fname) as f:
        raw = f.read().splitlines()
        num_contributors, num_projects = list(map(int, raw[0].split()))
        contributors, projects = [], []
        i = 1
        for j in range(num_contributors):
            name, num_skills = raw[i].split()
            num_skills = int(num_skills)
            skills = []
            i += 1
            for _ in range(num_skills):
                skills.append(parse_skill(raw[i]))
                i += 1
            contributors.append(Contributor(j, name, num_skills, {x.name: x.level for x in skills}))
        for j in range(num_projects):
            raw_project = raw[i].split()
            name = raw_project[0]
            num_days, score, best_before, num_roles = list(map(int, raw_project[1:]))
            i += 1
            requirements = []
            for _ in range(num_roles):
                requirements.append(parse_skill(raw[i]))
                i += 1
            projects.append(Project(j, name, num_days, score, best_before, num_roles, requirements))
        return Meta(os.path.basename(fname), num_contributors, num_projects), contributors, projects

def format_output(out):
    r = []
    r.append(f'{len(out)}')
    for s in out:
        r.append(s.name)
        r.append(' '.join([c.name for c in s.contributors]))
    return r<jupyter_output><empty_output><jupyter_text>Scoring function<jupyter_code>def score(parsed_input, sol):
    meta, contributors, projects  = parsed_input
    contributor_t2r = defaultdict(int)
    project_lookup = {p.name: p for p in projects}
    pending = set(s.name for s in sol)
    contributors_needed = {s.name: set(c.id for c in s.contributors) for s in sol}
    all_needed_contributors = set.union(*contributors_needed.values())
    cur_score = 0
    max_time = max([p.best_before + p.num_days for p in projects if p.name in pending])
    for t in tqdm(range(max_time)):
        not_available = set(cid for cid in all_needed_contributors if contributor_t2r[cid] > t)
        for s in filter(lambda x: x.name in pending, sol):
            if not any(c.id in not_available for c in s.contributors):
                # start project
                pending.remove(s.name)
                for c in s.contributors:
                    contributor_t2r[c.id] = t + project_lookup[s.name].num_days
                delay = max(0, t + project_lookup[s.name].num_days - project_lookup[s.name].best_before)
                cur_score += max(0, project_lookup[s.name].score - delay)
            not_available.update(contributors_needed[s.name])
    return cur_score<jupyter_output><empty_output><jupyter_text>Compute solution here. Solution runner only executes `compute`.<jupyter_code>from copy import copy

def has_skill(c, r):
    if r.name in c.skills:
        return c.skills[r.name] >= r.level
    return False

def skill_delta(c, r):
    if r.name in c.skills:
        return c.skills[r.name] - r.level
    return -1

def compute_greedy(parsed_input):
    """
    Greedy solution that matches people to the project that have the minimum required skill
    No learning, no mentoring
    """
    meta, contributors, projects = parsed_input
    list_of_schedules = []
 
    for p in projects:
        available = copy(contributors)
        temp_contributors = []
        for r in p.requirements:
            possible_contributor = list(filter(lambda c: has_skill(c, r), available))
            if possible_contributor:
                best_pc = possible_contributor[0]
                best_delta = skill_delta(best_pc, r)
                for pc in possible_contributor:
                    if cur_skill_delta := skill_delta(pc, r) < best_delta:
                        best_pc = pc
                        best_delta = cur_skill_delta
                temp_contributors.append(best_pc)
                available.remove(best_pc)
        if len(temp_contributors) == len(p.requirements):
            list_of_schedules.append(Schedule(p.name, temp_contributors))
    return list_of_schedules

def compute_with_simulation(parsed_input):
    """
    Simulate work days and schedule possible projects
    Note: runtime is way to long even with subsampling.
    """
    meta, contributors, projects  = parsed_input
    max_time = max([p.best_before + p.num_days for p in projects])
    contributor_t2r = {c.id: 0 for c in contributors}
    project_lookup = {p.name: p for p in projects}
    pending = projects
    list_of_schedules = []
    for t in tqdm(range(max_time)):
        new_pending  = []
        for p in pending:
            delay = max(0, t + p.num_days - p.best_before)
            if max(0, p.score - delay) > 0:
                new_pending.append(p)
        pending = new_pending
        for p in pending:#random.sample(pending, min(len(pending), 100)):
            available = [contributors[cid] for cid, v in contributor_t2r.items() if v == 0]
            #available = random.sample(available, min(len(available), 100))
            temp_contributors = []
            for r in p.requirements:
                possible_contributor = next(filter(lambda c: has_skill(c, r), available), None)
                if possible_contributor:
                    temp_contributors.append(possible_contributor)
                    available.remove(possible_contributor)
                    
            if len(temp_contributors) == len(p.requirements):
                list_of_schedules.append(Schedule(p.name, temp_contributors))
                pending.remove(p)
                for c in temp_contributors:
                    contributor_t2r[c.id] = p.num_days
        for c, v in contributor_t2r.items():
            contributor_t2r[c] = max(0, v - 1)
    return list_of_schedules

def compute_greedy_project_sorting(parsed_input):
    """
    Greedy solution that matches contributors to the project that have required skill.
    Projects and contributors are pre-sorted based on the different files with some manually found heuristic.
    Learning and mentoring included.
    """
    PARAMS = {
        'a': (lambda p: p.best_before, lambda c: -sum(c.skills.values())),
        'b': (lambda p: p.num_days, lambda c: -len(c.skills.values())),
        'c': (lambda p: -p.score / p.num_days, lambda c: -max(c.skills.values())),
        'd': (lambda p: -max(r.level for r in p.requirements), lambda c: -sum(c.skills.values())),
        'e': (lambda p: p.best_before, lambda c: -sum(c.skills.values())),
        'f': (lambda p: -p.score / p.num_days, lambda c: -max(c.skills.values()))
    }
    meta, contributors, projects = parsed_input
    list_of_schedules = []
    sorted_projects = sorted(projects, key=PARAMS[meta.file_name][0])
    sorted_contributors = sorted(contributors, key=PARAMS[meta.file_name][1])
 
    for p in tqdm(sorted_projects):
        available = copy(sorted_contributors)
        temp_contributors = []
        temp_other_skills = {}
        for r in p.requirements:
            req_lvl = r.level - 1 if temp_other_skills.get(r.name, -1) >= r.level else r.level
            possible_contributor = next(filter(lambda c: has_skill(c, Skill(r.name, r.level)), available), None)
            if possible_contributor:
                temp_contributors.append((possible_contributor, r))
                available.remove(possible_contributor)
                for cs, cs_lvl in possible_contributor.skills.items():
                    temp_other_skills[cs] = max(temp_other_skills.get(cs, -1), cs_lvl)
        if len(temp_contributors) == len(p.requirements):
            list_of_schedules.append(Schedule(p.name, [x[0] for x in temp_contributors]))
            for c, r in temp_contributors:
                if c.skills[r.name] - 1 < r.level:
                    c.skills[r.name] += 1
    return list_of_schedules

def enhance_solution(parsed_input, sol):
    pass

def compute(parsed_input):
    FUNC_LOOKUP = {
        'a': compute_greedy_project_sorting,
        'b': compute_greedy_project_sorting,
        'c': compute_greedy_project_sorting,
        'd': compute_greedy_project_sorting,
        'e': compute_greedy_project_sorting,
        'f': compute_greedy_project_sorting
    }
    meta, contributors, projects = parsed_input
    print(meta)
    #print(contributors)
    #print(projects)
    sol = FUNC_LOOKUP[os.path.basename(meta.file_name)](parsed_input)
    return sol<jupyter_output><empty_output><jupyter_text>Run<jupyter_code>s = new_sol()<jupyter_output>['a', 'b', 'c', 'd', 'e', 'f']
Meta(file_name='a', num_contributors=3, num_projects=3)<jupyter_text>---  Example<jupyter_code>f = os.path.join(INPUT_PATH, 'a')
data = open(f).read().splitlines()
data
parsed_input = parse_input(f)
meta, contributors, projects = parsed_input
meta, contributors, projects
sol = compute(parsed_input)
sol
score(parsed_input, sol)<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.1192012556679456, 'fraction_numerical': 0.004272758981513777, 'mean_word_length': 2.8281041388518022, 'pattern_counts': {'":': 0, '<': 30, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '7', 'dir': 'jupyter-structured-clean-dedup', 'id': '341864', 'license': 'MIT', 'n_tokens_mistral': 3788, 'n_tokens_neox': 3684, 'n_words': 856, 'path': 'solutions/163754_version_937372449042514445)/hashcode2022.ipynb', 'repo_name': 'englhardt/hashcode2022', 'size': '11468'}