You sure? I just did a Python dict matrix of single types, e.g. {'Normal': {'Rock': 0.5, 'Ghost': 0, 'Steel': 0.5}, ...}, then you can calculate dual type effectiveness like: for def_type in [def_type_1, def_type_2]: defender_multiplier *= get_effectiveness(att_type, def_type)
No need to store the outcomes for every dual-type interaction, you only need to count the resistances scored by each defender in order to pick a best candidate to proceed with on each iteration. You still need to store which "attackers" have already been dealt with as the while-loop runs, but that isn't too heavy as there are only 153 of them.
I think the best way to maximise this approach would be to have the greedy algorithm on each iteration randomly pick a defender from a pool that's within a certain % of the top-scoring candidate, then run the whole thing thousands of times. Introducing randomness and running it many times should allow it uncover some veins of type combinations that are efficient. But obviously it cannot ever prove what it found was the lowest.
I'll go ask AI to write that for me, unless you're really keen to work on your own algorithm without seeing any other results?