Unable to load image

day 16 aoc thread: the np is complete

this is travelling salesmen, eh? i'm pretty sure at least.

14
Jump in the discussion.

No email address required.

i am unbelievably filtered :marseyitsover:

Jump in the discussion.

No email address required.

:#marseycheerup:

Jump in the discussion.

No email address required.

:marseygigaretard:

Jump in the discussion.

No email address required.

:marseydepressed:

Jump in the discussion.

No email address required.

:marseysmug2:

Jump in the discussion.

No email address required.

Well after a lot of time trying to find the bug, i realized that we are always supposed to start at 'AA', but instead i started at the first line of the input. Literally would have been done like after like 30 minutes if I had paid attention to that...

I solve part two simply by dfs through the part once, and then if time gets to zero, i restart the dfs.

Part one ran in 661ms, Part two in 38004ms. Could most likely really speed this up by turning nodes with a flow rate of 0, into longer tunnels, but really don't care anymore.


#include <iostream>
#include <vector>
#include <utility>
#include <tuple>
#include <unordered_map>
#include <map>
#include <format>
#include <chrono>

struct input_set {
	std::vector<int64_t> flow_rates;
	std::vector<std::vector<int64_t>> tunnels;
};

input_set test_set{
	{ 0, 13, 2, 20, 3, 0, 0, 22, 0, 21 },
	{{3,8,1},{2,0},{3,1},{2,0,4},{5,3},{4,6},{5,7},{6},{0,9},{8}} 
};

input_set real_set{
	{0,0,0,0,0,0,0,19,0,0,0,0,24,7,6,8,18,14,0,0,0,0,0,0,12,0,0,0,0,0,0,0,4,0,0,0,11,17,0,0,0,0,0,0,0,0,23,0,0,16,5,0,0,0,0,15},
	{{49,14},{55,47},{12,6},{45,24},{46,41},{13,34},{2,49},{22,40},{35,13},{48,24},{36,32},{50,28},{2},{21,5,51,8,27},{18,19,0,42,30},{38,34,52,20},{22},{41,54},{50,14},{32,14},{15,36},{53,13},{7,16},{49,38},{3,52,9,39},{31,55},{32,27},{13,26},{11,53},{44,50},{14,53},{37,25},{43,10,44,19,26},{36,39},{5,15},{8,50},{20,10,33,47},{40,31},{15,23},{33,24},{37,7},{17,4},{51,14},{53,32},{32,29},{3,55},{4},{1,36},{53,9},{23,0,6},{54,18,35,11,29},{42,13},{15,24},{43,21,48,28,30},{50,17},{45,25,1}}
};

typedef std::tuple<int64_t, int64_t, int64_t, int64_t> key_type;
typedef std::map<key_type, int64_t> map_type;

int64_t dfs(int64_t location, int64_t time, int64_t turned_on, map_type& dp, const input_set& input, int runs) {
	key_type key = std::make_tuple(runs, location, time, turned_on);
	if (dp.count(key) == 1) return dp[key];

	if (time < 1) {
		if (runs > 1) return dfs(53, 26, turned_on, dp, input, runs - 1);
		else return 0LL;
	}

	auto rate = input.flow_rates[location];
	int64_t ans{0};
	// turned on is a a bitmap with a 1 at bit location, if location is turned
	// we are checking if it is not turned on
	if ((turned_on & (1LL << location)) == 0 && rate > 0) {
		int64_t turned_on_new = turned_on + (1LL << location);
		ans = (time - 1) * rate + dfs(location, time - 1, turned_on_new, dp, input, runs);
	}
	for (auto next_location : input.tunnels[location]) {
		ans = std::max(dfs(next_location, time - 1, turned_on, dp, input, runs), ans);
	}

	dp[key] = ans;
	return ans;	
}

int main()
{
	map_type dp_test;
	map_type dp;
	map_type dp_p2;

	auto a = std::chrono::high_resolution_clock::now();
	std::cout << "Test:" << dfs(0, 30, 0, dp_test, test_set, 1) << '\n';
	auto b = std::chrono::high_resolution_clock::now();
	std::cout << "Part One:" << dfs(53, 30, 0, dp, real_set, 1) << '\n';
	auto c = std::chrono::high_resolution_clock::now();
	std::cout << "Part Two:" << dfs(53, 26, 0, dp_p2, real_set, 2) << '\n';
	auto d = std::chrono::high_resolution_clock::now();

	auto ba = std::chrono::duration_cast<std::chrono::microseconds>(b - a);
	auto cb = std::chrono::duration_cast<std::chrono::microseconds>(c - b);
	auto dc = std::chrono::duration_cast<std::chrono::microseconds>(d - c);

	std::cout << std::format("TIMES: TEST: {}ms PART ONE: {}ms PART TWO: {}ms", ba, cb, dc);
}


Jump in the discussion.

No email address required.

Well after a lot of time trying to find the bug, i realized that we are always supposed to start at 'AA', but instead i started at the first line of the input. Literally would have been done like after like 30 minutes if I had paid attention to that...

LMAO same. Though I got lucky in that the thing told me that my answer is too high, so it must've been a bug and not a wrong algorithm, and I was lowkey bothered by writing the code that extracted starting node name from the first node without an explicit instruction to do so, so it was on my mind, so I immediately reread the task and oh shit.

I solve part two simply by running it in serial, apparently that gives the same result as if they were ran in parallel.

Neat!

Jump in the discussion.

No email address required.

Mommy is soooo proud of you, sweaty. Let's put this sperg out up on the fridge with all your other failures.

Jump in the discussion.

No email address required.

part 1 optimized code

About 1.1s runtime on input, which is tremendous compared to early iterations of the code.

What I do is shortcut paths that take zero-flow nodes, prune those zero-flow nodes (all but AA), compute floyd-warshall and replace the actual graph by this one, which basically skips having to go to already activated nodes or travel through useless nodes. Also replaced my queue BFS with a recursive DFS since most of the BFS was spent inserting into the queue (like 89% lmao). Removed the useless dict to skip identical situations (happened literally only once ever).

![](/images/1671304521390218.webp)

Jump in the discussion.

No email address required.

Okay here's part 2. Tried a global variable approach because doing shit with recursion is usually painful, but ended up with very slow code and a result with 200k+ paths. Did it the recursive way and only 113 paths to search lmao.

I am officially unfiltered

![](/images/16713082009308453.webp)

Jump in the discussion.

No email address required.

Got a tip on part 2 :marseysuicide:

officially filtered:marseywall:

![](/images/16712333232558393.webp)

Jump in the discussion.

No email address required.

im just so fricking sick of my solution working on the test input but not the real input. it's happened like 4 days in a fricking row now. how am i supposed to debug this shit?

Jump in the discussion.

No email address required.

The old school way, like you'd do if you didn't have a test input at all!

By "doesn't work" do you mean that it gives a wrong answer? Is the answer too low (you could be using a wrong algo) or too high (you have a bug)? Have you noticed that the starting node is "AA" not the first line in input :marseyfacepalm: ?

Jump in the discussion.

No email address required.

NEVER MIND

I BEAT PART 1 HOLY SHIT

:#soyjackwow: :#soyjackwow: :#soyjackwow::#soyjackwow::#soyjackwow:

i originally had a thing that checked if the time spent was above 10 and pressure was less than 500, then it would abort (to stop it taking fucking forever). that worked on the test, but gave the wrong answer on the real input. i changed it to time > 15 instead and it worked, which was a much more reasonable reason to fail than the previous ones. pretty sure the code i wrote is extremely unholy; i barely even remember the parts i wrote at the start of the day. pretty happy tho

import parse
from typing import List
import networkx as nx

with open("d16i.txt") as f:
    input = f.read()

names: List[str] = [x[0] if x[0] != "SD\nValve ZX" else "ZX" for x in parse.findall("Valve {} has", input)]
flow_rates: List[int] = [x[0] for x in parse.findall("flow rate={:d}", input)]
tunnels = {}
for i, line in enumerate(input.split("\n")):
    tunnels[names[i]] = tuple(thing[:2] for thing in line.split(" ")[9:]) 
tlm = {name: flow_rate for name, flow_rate in zip(names, flow_rates) if flow_rate > 0 or name == "AA"}

graph1 = nx.Graph()
graph2 = nx.Graph()

for name in names:
    graph1.add_node(name)

for name, connections in tunnels.items():
    for connection in connections:
        graph1.add_edge(name, connection)

for name in tlm:
    graph2.add_node(name)

for name in tlm:
    graph2.add_weighted_edges_from([(name, valve, nx.shortest_path_length(graph1, name, valve)) for valve in tlm])

def add_pressure(valves):
    turned = set(valve for valve in valves if valves.count(valve) > 1)
    return sum(tlm[valve] for valve in turned)

all_paths = []
def create_paths(this_path=["AA"], time_spent=0, pressure=0):
    if time_spent > 15 and pressure < 500:
        return
    if time_spent == 30:
        all_paths.append(pressure)
        return
    choices = []
    for valve in tlm:
        if valve == this_path[-1]:
            if not this_path.count(valve) > 1:
                choices.append(valve)
        elif not graph2.get_edge_data(this_path[-1], valve)["weight"] + time_spent > 30 and valve not in this_path:
            choices.append(valve)
    if not choices:
        while time_spent < 30:
            time_spent += 1
            pressure += add_pressure(this_path)
        all_paths.append(pressure)
    for choice in choices:
        if choice == this_path[-1]:
            create_paths(this_path + [choice], time_spent + 1, pressure + add_pressure(this_path))
        else:
            mins = graph2.get_edge_data(this_path[-1], choice)["weight"]
            add_p = 0
            for _ in range(mins):
                add_p += add_pressure(this_path)
            create_paths(this_path + [choice], time_spent + mins, pressure + add_p)

create_paths()
print(sorted(all_paths)[-1])
Jump in the discussion.

No email address required.

Congratulations! Btw, you don't have to explicitly call graph.add_node unless you want to add attributes on it or something.

Jump in the discussion.

No email address required.

@Bussy-boy frick you loser

Jump in the discussion.

No email address required.

All those words won't bring daddy back.

Jump in the discussion.

No email address required.

:#marseydunkon:

Jump in the discussion.

No email address required.

Have a solution that works for the test case in p1, but not sure if it works with the real data or just take a bit.

The idea is basically a depth first search and some memoization to store path we have already tried.

Jump in the discussion.

No email address required.

idk if that will work well. they could add weird cases were half going into a branch and later completing it is the best solution.

if you look at the dataset, u can notice there's a lot of rate=0s. i'm thinking maybe turning this into a simplified graph that just represents all distances between all valves with r>0, turning this into almost a raw traveling salesmen.

at that point u just brute force dfs it cause there's no simpler solution possible. at max cost=30, it's not a big issue i don't think.

unless u can solve np. in which case, i'm so down for revolutionizing cs in the process. :marseyparty2:

Jump in the discussion.

No email address required.

My solution actually works fast now, the issue was that the hashmap I used for memo used a shitty hash function, my results are still wrong, but I believe that is due to me entering the input wrong. Had to do other things though so couldn't test it properly so far.

But the other issues is that you can visit the same node multiple times, you can just not turn the valve more than once. This massively increase the amount of possible routes, which forces you into some kind memo solution.

Jump in the discussion.

No email address required.

i don't think u can use memoizing to significantly speed up an np-complete problem into not something np, not without inadvertently missing parts. that's kind of why they are np-complete. ur also going to be unbelievably fricked by part 2

my answer to p1 takes < 60ms in js of all things.

Jump in the discussion.

No email address required.

my answer to p1 takes < 60ms in js of all things.

Are you on the leaderboard?

I'm confused by several people who apparently solved part 1 by simply running DFS on a reduced graph. It still has 15 nodes (plus technically a separate starting node but that doesn't matter), so if you enumerate all possible routes, without pruning the search somehow, that's 15! ~= 10^12 of them, which you can't do in any reasonable amount of time. So how does your code work?

Myself, I did this: you can do dynamic programming using (current node, set of opened valves) and implied current turn as a key. That is, if you know that you can open valve #5 at turn 10 while having opened valves 3 and 7 already, with a maximum score of whatever, then you can ignore how you got to that state. And if you have a priority queue keyed by current turn and steadily work through it, then that's your correct DP computation order (though I somehow convinced myself that I need some extra complications). So now instead of 15! I'm traversing a graph with 15 * 2^15 nodes (plus some spread of future current turns in the queue).

Jump in the discussion.

No email address required.

15! ~= 10^12

u can't actually enumerate out that many cause of the max cost. if you jump across the map and back - it will be very few nodes you enumerate out. if they removed max cost then i suppose it wouldn't be feasible.

So now instead of 15! I'm traversing a graph with 15 * 2^15 nodes (plus some spread of future current turns in the queue).

i'm prolly gunna have to do the dynamic approach cause p2 with plain recursion is kinda nonsense.

Jump in the discussion.

No email address required.

Oh, I see. Hmmm. Interesting.

Note how I did part 2 in 15 minutes which might be a hint in itself :marseysmug2:

Jump in the discussion.

No email address required.

>finally catch back up to current day with a few hours to go :marseyexcited:

>graphshit :marseysad:

APLsisters, this might be the end of the line.....

Jump in the discussion.

No email address required.

Not just graphshit but NP-hard graphshit. Good luck.

Jump in the discussion.

No email address required.

Goddamn this one was a fucking nightmare. I thought I was hot shit realizing that you can turn the problem into travelling salesman based on the shortest paths between all nonzero valves since you can reasonably assume you'll be beelining between them at all times, but figuring out how to adapt to multiple salesmen took me ages despite it being fairly simple in the end. Just glad I squeezed through the filter tbh.

import re

class Valve:
    def __init__(self, id, name, flow):
        self.name = name
        self.id = id
        self.flow = flow
        self.out_dir = []

foo, valves, valves_of_interest = [], [], []
start_valve = None
with open('day16_input.txt', 'r') as inp:
    foo = inp.read().split('\n')

for f in range(len(foo)):
    foo[f] = re.split(' |=', foo[f].replace(',','').replace(';',''))
    valves.append(Valve(f, foo[f][1], int(foo[f][5])))
    if foo[f][1] == 'AA':
        start_valve = valves[-1]
    elif int(foo[f][5]) > 0:
        valves_of_interest.append(valves[-1])
for f in range(len(foo)):
    for v in range(len(foo)):
        if f != v and valves[v].name in foo[f][10:]:
            valves[v].out_dir.append(valves[f])

dist = [[1000 for x in range(len(valves))] for y in range(len(valves))]
for v in valves:
    dist[v.id][v.id] = 0
    check = [v]
    n = 1
    while len(check) > 0:
        for c in range(len(check)):
            for o in check[0].out_dir:
                if dist[v.id][o.id] > n:
                    dist[v.id][o.id] = n
                    check.append(o)
            check = check[1:]
        n += 1

solutions = []
def max_pressure(current_valve, score, elapsed, visited, turn_count, record_sols):
    if record_sols:
        global solutions
        solutions.append([score, visited])
    vals = []
    for v in valves_of_interest:
        time_after = dist[current_valve.id][v.id]+elapsed+1
        if v is not current_valve and time_after <= turn_count and v.id not in visited:
            vals.append(max_pressure(v, score+(v.flow*(turn_count-time_after)), time_after, visited + [v.id], turn_count, record_sols))
    if len(vals) == 0:
        return score
    return max(vals)

print(max_pressure(start_valve, 0, 0, [], 30, False)) 

m = 0
count = 0
max_pressure(start_valve, 0, 0, [], 26, True)
for s in solutions:
    second_go = max_pressure(start_valve, s[0], 0, s[1], 26, False)
    if second_go > m:
        m = second_go
print(m)
Jump in the discussion.

No email address required.

You sat down and wrote all this shit. You could have done so many other things with your life. What happened to your life that made you decide writing novels of bullshit here was the best option?

Jump in the discussion.

No email address required.

AOC's Nipples?

Jump in the discussion.

No email address required.

Sneeds-Feeduck-And-Seeduck make an account already!

Jump in the discussion.

No email address required.

Oh boy. More CGI nonsense for mentally underdeveloped manchildren. Wonderful.

The MCU films have become an endless and risk-free pack of Taco Bell mild sauce that is slowly dumbing down moviegoers and re-calibrating what a blockbuster film can be.

Disney is a black hole swallowing everything in its path until it is the only universe we have left. I bet you dollars to donuts no one here saw Silence or The Nice Guys in theaters but borrowed their dad's Subaru so you could see Ant-Man and the Wasp opening weekend.

I come from a generation where a creative "face-swapping" blockbuster film with $100M+ budget and an R-Rating from a foreign director could get a prime wide release date and make money. Now because of infantile consoomers who routinely get excited to pay for toy commercials with DoD propaganda, budgets are being slashed, young filmmakers are selling out, legends are relegated to streaming, and less people are getting laid.

To be clear, if you're a grown adult and you're genuinely excited for a fringe piece of shit superhero movie like Spider-Man: We Brought Back the Gambling Addict Who Dates Women Half His Age, you're a goddarn useless dork.

The MCU is crack for dumb people. They got you strung out and morons just blindly line up saying “the last couple MCU flicks have been lame, but I've seen all 47 of them to this point so I better watch Man-Ant vs The Gobots vs Dr Doom!”

McDonalds is quick, easy, cheap, and completely average in every way. Sure, from time to time it hits the spot but I usually end up feeling like shit and my butt leaks for 24 hours. It's more work, more of a challenge, and sometimes can be disappointing but I'd much prefer to seek out a unique burger joint that at least will try to offer their own spin on things. I'm not 100% sure what I'll get but it might be something interesting. Now everything is built on franchise recognition and familiarity and more and more people are conceding everyday - which puts my ability to go to my kind of restaurant at risk. Companies follow the money and may offer the occasional artisan option - but if it ain't, selling it'll be replaced something easier to sell. Heck, these lazy morons don't even dine out anymore - they sit on their couch and have their compressed, shitty, and inoffensive content delivered directly to their homes.

Happy Meal fricks who justify watching a decade of toy commercials and hand every nickel to corporations who call movies “content” not realizing they're setting the rest of us up are the worst.

Watch what you want, but when I can't see something like First Reformed or Portrait of a Lady on Fire or American Animals or Only God Forgives on a big screen because the theaters have got 16 screens all playing Hawkeye vs Han Solo, I'm blaming you.

Jump in the discussion.

No email address required.

Link copied to clipboard
Action successful!
Error, please refresh the page and try again.