Files
dusklight/mods/randomizer/generator/logic/flatten/simplify_algebraic.cpp
T
2026-07-27 22:33:03 -07:00

425 lines
15 KiB
C++

#include "simplify_algebraic.hpp"
#include "../../utility/container.hpp"
// Turns a bit-based DNF (a two-level sum-of-products) back into
// a readable multi-level requirement.
randomizer::logic::requirement::Requirement DNFToExpr(BitIndex& bitIndex, DNF dnf)
{
if (dnf.isTriviallyFalse())
{
return randomizer::logic::requirement::Requirement {randomizer::logic::requirement::Type::IMPOSSIBLE, {}};
}
if (dnf.isTriviallyTrue())
{
return randomizer::logic::requirement::Requirement {randomizer::logic::requirement::Type::NOTHING, {}};
}
// really make sure no dupes exist, not sure if needed
dnf = dnf.dedup();
// Map to BitVectors. Since DNFs don't offer bit-level access,
// we have to manually go through every bit to build BitVectors.
// This is definitely not cheap but it probably saves more time
// than keeping the intsets around during search
std::vector<BitVector> expr = {};
for (const auto& t : dnf.terms)
{
std::list<int> bits = {};
for (int bit = 0; bit < bitIndex.counter; bit++)
{
if (t.test(bit))
{
bits.push_back(bit);
}
}
expr.emplace_back(bits);
}
// at this point we must remove weaker requirements. E.g.
// imagine Beedle existed in this rando and an item required
// (Wallet x1 and Wallet x2) or (Wallet x1 and ExtraWallet x1 and ExtraWallet x2)
// then this code would pull out Wallet x1 first, resulting in
// Wallet x1 and (Wallet x2 or ExtraWallet x1 and ExtraWallet x2) which is not
// reasonable at all and at that point not even the TWWR-Tracker simplifications can save us
for (auto& term : expr)
{
for (const auto& bit : term.ints())
{
auto& req = bitIndex.reverseIndex[bit];
if (req._type == randomizer::logic::requirement::Type::COUNT)
{
auto count = std::get<int>(req._args[0]);
auto item = std::get<randomizer::logic::item::Item*>(req._args[1]);
for (int i = 1; i < count; i++)
{
auto lesserBit = bitIndex.reqBit(
randomizer::logic::requirement::Requirement {randomizer::logic::requirement::Type::COUNT, {i, item}});
term.clear(lesserBit);
}
}
}
}
auto commonFactors = expr[0].ints();
for (const auto& term : expr)
{
std::set<int> intersection = {};
std::set_intersection(commonFactors.begin(),
commonFactors.end(),
term.intset.begin(),
term.intset.end(),
std::inserter(intersection, intersection.begin()));
commonFactors = intersection;
}
// build a list of variables that appear in our expression,
// excluding common factors.
std::set<int> varSet = {};
for (auto& term : expr)
{
for (const auto& c : commonFactors)
{
term.clear(c);
}
for (const auto& b : term.ints())
{
varSet.insert(b);
}
}
std::vector<int> variables = std::vector<int>(varSet.begin(), varSet.end());
if (variables.empty())
{
return createAnd(lookupRequirements(bitIndex, commonFactors));
}
std::vector<BitVector> seen = {};
auto kernels = findKernels(expr, variables, BitVector(), seen);
kernels = randomizer::utility::container::FilterFromVector(kernels, [](const auto& k) { return !k.coKernel.isEmpty(); });
// columns are unique cubes in all kernels
std::vector<BitVector> columns = {};
for (const auto& kernel : kernels)
{
for (const auto& kCube : kernel.kernel)
{
if (std::none_of(columns.begin(), columns.end(), [&](const auto& c) { return kCube.equals(c); }))
{
columns.push_back(kCube);
}
}
}
// rows are unique co-kernels
auto& rows = kernels;
if (!rows.empty() && !columns.empty())
{
std::vector<std::vector<int>> matrix = {};
for (const auto& row : rows)
{
matrix.emplace_back(columns.size(), 0);
}
// create a matrix that is 1 where column cubes appear in row kernels.
// since kernels are the result of a single division (by the co-kernel),
// this essentially creates ones where division by another cube would be possible
for (int col = 0; col < columns.size(); col++)
{
auto& kCube = columns[col];
for (int row = 0; row < rows.size(); row++)
{
auto& coKernel = rows[row];
if (std::any_of(coKernel.kernel.begin(), coKernel.kernel.end(), [&](const auto& k) { return kCube.equals(k); }))
{
matrix[row][col] = 1;
}
}
}
// Find the best rectangle. This optimizes for #literals saved
// in the resulting expression, which is a good heuristic for
// minimizing the length of the expression.
auto rowWeight = [&](const int& row) { return rows[row].coKernel.size() + 1; };
auto colWeight = [&](const int& col) { return columns[col].size(); };
auto value = [&](const int& col, const int& row)
{
auto cpy = rows[row].coKernel;
cpy.or_(columns[col]);
return cpy.size();
};
auto literalsSaved = [&](const std::tuple<std::vector<int>, std::vector<int>>& rect)
{
auto [rectRows, rectCols] = rect;
int weight = 0;
for (const auto& row : rectRows)
{
for (const auto& col : rectCols)
{
if (matrix[row][col])
{
weight += value(col, row);
}
}
}
for (const auto& row : rectRows)
{
weight -= rowWeight(row);
}
for (const auto& col : rectCols)
{
weight -= colWeight(col);
}
return weight;
};
std::vector<std::tuple<std::vector<int>, std::vector<int>>> allRects = {};
std::vector<int> rows_;
std::vector<int> cols_;
for (int i = 0; i < rows.size(); i++)
{
rows_.push_back(i);
}
for (int i = 0; i < columns.size(); i++)
{
cols_.push_back(i);
}
genRectangles(rows_,
cols_,
matrix,
[&](const std::vector<int>& rows__, const std::vector<int>& cols__)
{ allRects.push_back({rows__, cols__}); });
if (!allRects.empty())
{
auto& [bestRows, bestCols] = *std::max_element(allRects.begin(),
allRects.end(),
[&](const auto& rect1, const auto& rect2)
{ return literalsSaved(rect1) < literalsSaved(rect2); });
// divisor is created by OR-ing column cubes
std::vector<BitVector> divisor = {};
for (const auto& c : bestCols)
{
divisor.push_back(columns[c]);
}
auto [quot, remainder] = algebraicDivision(expr, divisor);
// and re-assemble a Requirement that sort of looks like
// common_factors * (quotient * divisor + remainder)
auto product = randomizer::logic::requirement::Requirement();
product._type = randomizer::logic::requirement::Type::AND;
std::vector<std::bitset<512>> quotBits;
std::vector<std::bitset<512>> divisorBits;
for (const auto& c : quot)
{
quotBits.push_back(c.bitset);
}
for (const auto& c : divisor)
{
divisorBits.push_back(c.bitset);
}
product._args.push_back(DNFToExpr(bitIndex, DNF(quotBits)));
product._args.push_back(DNFToExpr(bitIndex, DNF(divisorBits)));
auto sum = randomizer::logic::requirement::Requirement();
if (!remainder.empty())
{
std::vector<std::bitset<512>> remainderBits;
for (const auto& c : remainder)
{
remainderBits.push_back(c.bitset);
}
sum._type = randomizer::logic::requirement::Type::OR;
sum._args.push_back(product);
sum._args.push_back(DNFToExpr(bitIndex, DNF(remainderBits)));
}
else
{
sum = product;
}
auto terms = lookupRequirements(bitIndex, commonFactors);
terms.push_back(sum);
return createAnd(terms);
}
}
// here we didn't do our complicated rectangle extraction, so just extract
// the common factors
auto terms = randomizer::logic::requirement::Requirement();
terms._type = randomizer::logic::requirement::Type::OR;
for (const auto& c : expr)
{
terms._args.push_back(createAnd(lookupRequirements(bitIndex, c.ints())));
}
// common_factor1 AND common_factor2 AND ... AND (terms without common factors ORed)
auto finalTerms = lookupRequirements(bitIndex, commonFactors);
finalTerms.push_back(terms);
return createAnd(finalTerms);
}
randomizer::logic::requirement::Requirement createAnd(std::vector<randomizer::logic::requirement::Requirement> terms)
{
if (terms.size() > 1)
{
randomizer::logic::requirement::Requirement req;
req._type = randomizer::logic::requirement::Type::AND;
for (auto& term : terms)
{
req._args.push_back(term);
}
return req;
}
return terms[0];
}
// Recursively computes kernels and co-kernels of the expression `cubes`.
// A co-kernel is a cube (product term) such that for `expr / co-kernel = kernel`,
// `kernel` contains at least two terms but there's no factor to factor out.
// This effectively tries every combination of variables in this expression
// as a co-kernel. seenCoKernels is a bit of book-keeping to not create
// duplicate kernels, and min_idx ensures we don't try e.g. ab and ba separately
std::vector<FoundKernel> findKernels(const std::vector<BitVector>& cubes,
const std::vector<int>& variables,
const BitVector& coKernelPath,
std::vector<BitVector>& seenCoKernels,
int minIdx /* = 0 */)
{
std::vector<FoundKernel> kernels = {};
for (int idx = 0; idx < variables.size(); idx++)
{
auto& bit = variables[idx];
// we won't find any useful kernels by trying these *again*
if (idx < minIdx)
{
continue;
}
std::vector<BitVector> s = {};
for (auto& c : cubes)
{
if (c.test(bit))
{
s.push_back(c);
}
}
if (s.size() >= 2)
{
auto co = s[0];
for (const auto& c : s)
{
co.and_(c);
}
auto subPath = coKernelPath;
subPath.or_(co);
auto [quot, remainder] = algebraicDivision(cubes, {co});
auto subKernels = findKernels(quot, variables, subPath, seenCoKernels, idx + 1);
for (const auto& sub : subKernels)
{
if (std::none_of(seenCoKernels.begin(),
seenCoKernels.end(),
[=](const auto& seenCo) { return seenCo.equals(sub.coKernel); }))
{
seenCoKernels.push_back(sub.coKernel);
kernels.push_back(sub);
}
}
}
}
// cube-free expr is always its own kernel, with trivial co-kernel 1
if (std::none_of(seenCoKernels.begin(),
seenCoKernels.end(),
[=](const auto& seenCo) { return seenCo.equals(coKernelPath); }))
{
kernels.push_back(FoundKernel {cubes, coKernelPath});
}
return kernels;
}
// Computes the algebraic division of expr / divisor, returning
// the quotient and the remainder. These satisfy the formula
// expr = quotient * divisor + remainder
std::pair<std::vector<BitVector>, std::vector<BitVector>> algebraicDivision(const std::vector<BitVector>& expr,
const std::vector<BitVector>& divisor)
{
std::vector<BitVector> quot = {};
// for every "cube"/product term in our divisor...
for (const auto& divCube : divisor)
{
// get a list of all cubes that this can be divided by
std::vector<BitVector> c = {};
std::copy_if(expr.begin(), expr.end(), std::back_inserter(c), [=](const auto& e) { return divCube.isSubsetOf(e); });
if (c.empty())
{
// division not possible, remainder is the entire expression
return {{}, expr};
}
// "cross out" the bits of this divisor cube
for (auto& ci : c)
{
for (const auto& bit : divCube.ints())
{
ci.clear(bit);
}
}
// compute the intersection of the divided expr with the divided expr in other cubes
if (quot.empty())
{
quot = c;
}
else
{
// this is literally set intersection, NOT an OR or an AND
std::vector<BitVector> newQuot = {};
std::copy_if(quot.begin(),
quot.end(),
std::back_inserter(newQuot),
[=](const auto& qc)
{ return std::any_of(c.begin(), c.end(), [=](const auto& cc) { return cc.equals(qc); }); });
quot = newQuot;
}
}
// finally, compute the remainder essentially by computing
// remainder = expr - quotient * divisor
// * is AND
std::vector<std::bitset<512>> quotBits = {};
for (auto& i : quot)
{
quotBits.push_back(i.bitset);
}
std::vector<std::bitset<512>> divisorBits = {};
for (auto& i : divisor)
{
divisorBits.push_back(i.bitset);
}
DNF product = DNF(quotBits).and_(DNF(divisorBits)).dedup();
std::vector<BitVector> remainder = {};
std::copy_if(expr.begin(),
expr.end(),
std::back_inserter(remainder),
[=](const auto& e)
{
return std::none_of(product.terms.begin(),
product.terms.end(),
[=](const auto& productTerm) { return includedIn(productTerm, e.bitset); });
});
return {quot, remainder};
}