From 92cb8ee859dcd7ecbb8aed7e9f8296f2192363e3 Mon Sep 17 00:00:00 2001
From: AzaelMew <66936848+AzaelMew@users.noreply.github.com>
Date: Tue, 28 Apr 2026 06:44:56 +0200
Subject: [PATCH] feat: add Subtractive BnB V2 as separate class
Optimisations over V1:
- Flat primitive arrays for req/bonus (better cache locality)
- Fast-path depth-0 and depth-1 search before general DFS
- Branch ordering by weight to find best tie-break first
- Special-case n=0 and n=1 to skip visited clearing and recursion
- Registered alongside V1
---
.../java/com/wynncraft/AlgorithmRegistry.java | 2 +
.../algorithms/SubtractiveBnBV2Algorithm.java | 527 ++++++++++++++++++
2 files changed, 529 insertions(+)
create mode 100644 src/main/java/com/wynncraft/algorithms/SubtractiveBnBV2Algorithm.java
diff --git a/src/main/java/com/wynncraft/AlgorithmRegistry.java b/src/main/java/com/wynncraft/AlgorithmRegistry.java
index 66386dc..550d528 100644
--- a/src/main/java/com/wynncraft/AlgorithmRegistry.java
+++ b/src/main/java/com/wynncraft/AlgorithmRegistry.java
@@ -4,6 +4,7 @@
import com.wynncraft.algorithms.NegativeOrderAlgorithm;
import com.wynncraft.algorithms.CapyTopoAlgorithm;
import com.wynncraft.algorithms.SubtractiveBnBAlgorithm;
+import com.wynncraft.algorithms.SubtractiveBnBV2Algorithm;
import com.wynncraft.algorithms.PrunedMaskAlgorithm;
import com.wynncraft.algorithms.PrunedMaskV2Algorithm;
import com.wynncraft.algorithms.StarvingGoblinAlgorithm;
@@ -33,6 +34,7 @@ public class AlgorithmRegistry {
register(new NegativeOrderAlgorithm(), WynnPlayer.Builder::new);
register(new PrunedMaskAlgorithm(), WynnPlayer.Builder::new);
register(new PrunedMaskV2Algorithm(), WynnPlayer.Builder::new);
+ register(new SubtractiveBnBV2Algorithm(), WynnPlayer.Builder::new);
register(new StarvingGoblinAlgorithm(), StarvingPlayer.Builder::new);
}
diff --git a/src/main/java/com/wynncraft/algorithms/SubtractiveBnBV2Algorithm.java b/src/main/java/com/wynncraft/algorithms/SubtractiveBnBV2Algorithm.java
new file mode 100644
index 0000000..cf76d27
--- /dev/null
+++ b/src/main/java/com/wynncraft/algorithms/SubtractiveBnBV2Algorithm.java
@@ -0,0 +1,527 @@
+package com.wynncraft.algorithms;
+
+import com.wynncraft.core.WynnPlayer;
+import com.wynncraft.core.interfaces.IAlgorithm;
+import com.wynncraft.core.interfaces.IEquipment;
+import com.wynncraft.core.interfaces.Information;
+import com.wynncraft.enums.SkillPoint;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Subtractive Branch-and-Bound with Witness-Driven Removal (SBBR) — V2.
+ *
+ *
Evolution of {@link SubtractiveBnBAlgorithm} with the following
+ * optimisations:
+ *
+ * - Flat arrays for req/bonus (better cache locality than
+ * jagged {@code int[][]}).
+ * - Fast-path depth-0/1: explicit validity checks for the full
+ * set and for every single-item-removal before falling back to DFS.
+ * On typical builds this avoids recursion entirely.
+ * - Branch ordering: candidates are sorted by ascending weight
+ * (most-negative first) so the highest tie-break is found early,
+ * pruning the remaining branches aggressively.
+ * - Small-n short-circuit: {@code n ≤ 1} skips visited-bitset
+ * clearing and recursion overhead completely.
+ *
+ *
+ * All other semantics (cascade, ordering, no deduplication) are identical
+ * to V1.
+ */
+@Information(name = "Subtractive BnB", version = 2, authors = {"Azael"})
+public final class SubtractiveBnBV2Algorithm implements IAlgorithm {
+
+ private static final int SK = SkillPoint.values().length;
+ private static final SkillPoint[] SP_VALS = SkillPoint.values();
+
+ private static final int N_VISITED_BITS = 22;
+ private static final int VISITED_WORDS = (1 << N_VISITED_BITS) / 64;
+
+ /* ── Flat per-run arrays ── */
+ private int[] reqFlat = new int[0];
+ private int[] bonusFlat = new int[0];
+ private int[] weights = new int[0];
+ private IEquipment[] itemArr = new IEquipment[0];
+ private int n;
+
+ private final int[] T = new int[SK];
+ private final int[] manual = new int[SK];
+
+ private final long[] negMask = new long[SK];
+ private long noReqMask;
+
+ /* ── Best solution tracking ── */
+ private int bestCount;
+ private int bestWeight;
+ private long bestMask;
+
+ /* ── Visited state ── */
+ private final long[] visited = new long[VISITED_WORDS];
+ private boolean useVisited;
+
+ /* ── Incremental setup cache ── */
+ private int cachedN = 0;
+ private IEquipment[] cachedItemRefs = new IEquipment[0];
+ private final int[] cachedManual = new int[SK];
+ private int cachedTotalWeight = 0;
+
+ /* ── Branch-order scratch buffer ── */
+ private final int[] branchOrder = new int[64];
+
+ @Override
+ public Result run(WynnPlayer player) {
+ List equipment = player.equipment();
+ int eqSize = equipment.size();
+ n = eqSize;
+
+ for (int s = 0; s < SK; s++) manual[s] = player.allocated(SP_VALS[s]);
+
+ if (reqFlat.length < eqSize * SK) {
+ int cap = Math.max(eqSize, 32);
+ reqFlat = new int[cap * SK];
+ bonusFlat = new int[cap * SK];
+ weights = new int[cap];
+ itemArr = new IEquipment[cap];
+ }
+
+ int startFrom = 0;
+ if (n >= cachedN && cachedN > 0) {
+ startFrom = cachedN;
+ for (int i = 0; i < cachedN; i++) {
+ if (equipment.get(i) != cachedItemRefs[i]) { startFrom = 0; break; }
+ }
+ }
+
+ if (startFrom == 0) {
+ noReqMask = 0L;
+ Arrays.fill(negMask, 0L);
+ for (int s = 0; s < SK; s++) T[s] = manual[s];
+ cachedTotalWeight = 0;
+ for (int i = 0; i < n; i++) {
+ setupItem(i, equipment.get(i));
+ cachedTotalWeight += weights[i];
+ T[0] += bonusFlat[i * SK];
+ T[1] += bonusFlat[i * SK + 1];
+ T[2] += bonusFlat[i * SK + 2];
+ T[3] += bonusFlat[i * SK + 3];
+ T[4] += bonusFlat[i * SK + 4];
+ }
+ } else {
+ T[0] += manual[0] - cachedManual[0];
+ T[1] += manual[1] - cachedManual[1];
+ T[2] += manual[2] - cachedManual[2];
+ T[3] += manual[3] - cachedManual[3];
+ T[4] += manual[4] - cachedManual[4];
+ for (int i = startFrom; i < n; i++) {
+ setupItem(i, equipment.get(i));
+ cachedTotalWeight += weights[i];
+ T[0] += bonusFlat[i * SK];
+ T[1] += bonusFlat[i * SK + 1];
+ T[2] += bonusFlat[i * SK + 2];
+ T[3] += bonusFlat[i * SK + 3];
+ T[4] += bonusFlat[i * SK + 4];
+ }
+ }
+
+ if (cachedItemRefs.length < n) cachedItemRefs = new IEquipment[Math.max(n, 32)];
+ for (int i = startFrom; i < n; i++) cachedItemRefs[i] = itemArr[i];
+ cachedN = n;
+ System.arraycopy(manual, 0, cachedManual, 0, SK);
+
+ long fullMask = (n == 64) ? -1L : (1L << n) - 1L;
+
+ bestCount = -1;
+ bestWeight = Integer.MIN_VALUE;
+ bestMask = 0L;
+
+ /* Fast path 0: full set valid */
+ boolean needsDfs = true;
+ if (cascadePass(fullMask) && orderingPass(fullMask)) {
+ bestCount = n;
+ bestWeight = cachedTotalWeight;
+ bestMask = fullMask;
+ needsDfs = false;
+ }
+
+ /* Fast path for n = 0 or 1 */
+ if (needsDfs && n <= 1) {
+ bestCount = 0;
+ bestWeight = 0;
+ bestMask = 0L;
+ needsDfs = false;
+ }
+
+ /* Fast path 1: single removal */
+ if (needsDfs && n > 1) {
+ int worstI = -1, worstS = -1, worstD = 0;
+ long iter = fullMask & ~noReqMask;
+ while (iter != 0L) {
+ long bit = iter & -iter; iter ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (reqFlat[b] > 0) {
+ int d = reqFlat[b] + bonusFlat[b] - T[0];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 0; }
+ }
+ if (reqFlat[b + 1] > 0) {
+ int d = reqFlat[b + 1] + bonusFlat[b + 1] - T[1];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 1; }
+ }
+ if (reqFlat[b + 2] > 0) {
+ int d = reqFlat[b + 2] + bonusFlat[b + 2] - T[2];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 2; }
+ }
+ if (reqFlat[b + 3] > 0) {
+ int d = reqFlat[b + 3] + bonusFlat[b + 3] - T[3];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 3; }
+ }
+ if (reqFlat[b + 4] > 0) {
+ int d = reqFlat[b + 4] + bonusFlat[b + 4] - T[4];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 4; }
+ }
+ }
+
+ if (worstI != -1) {
+ int bc = 0;
+ branchOrder[bc++] = worstI;
+ long negs = fullMask & negMask[worstS] & ~(1L << worstI);
+ while (negs != 0L) {
+ long bit = negs & -negs; negs ^= bit;
+ branchOrder[bc++] = Long.numberOfTrailingZeros(bit);
+ }
+ for (int a = 1; a < bc; a++) {
+ int key = branchOrder[a];
+ int j = a - 1;
+ while (j >= 0 && weights[branchOrder[j]] > weights[key]) {
+ branchOrder[j + 1] = branchOrder[j];
+ j--;
+ }
+ branchOrder[j + 1] = key;
+ }
+
+ for (int idx = 0; idx < bc; idx++) {
+ int i = branchOrder[idx];
+ int w = cachedTotalWeight - weights[i];
+ if (n - 1 < bestCount || (n - 1 == bestCount && w <= bestWeight)) continue;
+
+ int base = i * SK;
+ T[0] -= bonusFlat[base];
+ T[1] -= bonusFlat[base + 1];
+ T[2] -= bonusFlat[base + 2];
+ T[3] -= bonusFlat[base + 3];
+ T[4] -= bonusFlat[base + 4];
+
+ long m = fullMask ^ (1L << i);
+ if (cascadePass(m) && orderingPass(m)) {
+ bestCount = n - 1;
+ bestWeight = w;
+ bestMask = m;
+ }
+
+ T[0] += bonusFlat[base];
+ T[1] += bonusFlat[base + 1];
+ T[2] += bonusFlat[base + 2];
+ T[3] += bonusFlat[base + 3];
+ T[4] += bonusFlat[base + 4];
+ }
+
+ if (bestCount >= n - 1) {
+ needsDfs = false;
+ }
+ }
+ }
+
+ if (needsDfs) {
+ useVisited = (n <= N_VISITED_BITS);
+ if (useVisited) {
+ int wordsToClear = (1 << n) / 64 + 1;
+ if (wordsToClear > VISITED_WORDS) wordsToClear = VISITED_WORDS;
+ Arrays.fill(visited, 0, wordsToClear, 0L);
+ }
+ dfs(fullMask, n, cachedTotalWeight);
+ }
+
+ int validCount = (int) Long.bitCount(bestMask);
+ List valid = new ArrayList<>(validCount);
+ List invalid = new ArrayList<>(n - validCount);
+ for (int i = 0; i < n; i++) {
+ if ((bestMask & (1L << i)) != 0L) valid.add(itemArr[i]);
+ else invalid.add(itemArr[i]);
+ }
+
+ player.reset();
+ for (int i = 0, sz = valid.size(); i < sz; i++) {
+ player.modify(valid.get(i).bonuses(), true);
+ }
+
+ return new Result(valid, invalid);
+ }
+
+ private void setupItem(int i, IEquipment item) {
+ int[] r = item.requirements();
+ int[] b = item.bonuses();
+ int base = i * SK;
+ boolean hr = false;
+ int w = 0;
+ for (int s = 0; s < SK; s++) {
+ reqFlat[base + s] = r[s];
+ bonusFlat[base + s] = b[s];
+ if (r[s] > 0) hr = true;
+ if (b[s] < 0) negMask[s] |= (1L << i);
+ w += b[s];
+ }
+ weights[i] = w;
+ if (!hr) noReqMask |= (1L << i);
+ itemArr[i] = item;
+ }
+
+ private boolean cascadePass(long active) {
+ long iter = active & ~noReqMask;
+ while (iter != 0L) {
+ long bit = iter & -iter; iter ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (reqFlat[b] > 0 && reqFlat[b] + bonusFlat[b] > T[0]) return false;
+ if (reqFlat[b + 1] > 0 && reqFlat[b + 1] + bonusFlat[b + 1] > T[1]) return false;
+ if (reqFlat[b + 2] > 0 && reqFlat[b + 2] + bonusFlat[b + 2] > T[2]) return false;
+ if (reqFlat[b + 3] > 0 && reqFlat[b + 3] + bonusFlat[b + 3] > T[3]) return false;
+ if (reqFlat[b + 4] > 0 && reqFlat[b + 4] + bonusFlat[b + 4] > T[4]) return false;
+ }
+ return true;
+ }
+
+ private boolean orderingPass(long active) {
+ int os0 = manual[0], os1 = manual[1], os2 = manual[2], os3 = manual[3], os4 = manual[4];
+ long nra = active & noReqMask;
+ while (nra != 0L) {
+ long bit = nra & -nra; nra ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (bonusFlat[b] > 0) os0 += bonusFlat[b];
+ if (bonusFlat[b + 1] > 0) os1 += bonusFlat[b + 1];
+ if (bonusFlat[b + 2] > 0) os2 += bonusFlat[b + 2];
+ if (bonusFlat[b + 3] > 0) os3 += bonusFlat[b + 3];
+ if (bonusFlat[b + 4] > 0) os4 += bonusFlat[b + 4];
+ }
+
+ long remaining = active & ~noReqMask;
+ boolean progress = true;
+ while (progress && remaining != 0L) {
+ progress = false;
+ long ri2 = remaining;
+ while (ri2 != 0L) {
+ long bit = ri2 & -ri2; ri2 ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if ((reqFlat[b] <= 0 || os0 >= reqFlat[b]) &&
+ (reqFlat[b + 1] <= 0 || os1 >= reqFlat[b + 1]) &&
+ (reqFlat[b + 2] <= 0 || os2 >= reqFlat[b + 2]) &&
+ (reqFlat[b + 3] <= 0 || os3 >= reqFlat[b + 3]) &&
+ (reqFlat[b + 4] <= 0 || os4 >= reqFlat[b + 4])) {
+ os0 += bonusFlat[b];
+ os1 += bonusFlat[b + 1];
+ os2 += bonusFlat[b + 2];
+ os3 += bonusFlat[b + 3];
+ os4 += bonusFlat[b + 4];
+ remaining ^= bit;
+ progress = true;
+ }
+ }
+ }
+ return remaining == 0L;
+ }
+
+ private void dfs(long active, int count, int weight) {
+ if (count <= 0) {
+ if (bestCount < 0) { bestCount = 0; bestWeight = 0; bestMask = 0L; }
+ return;
+ }
+ if (count < bestCount || (count == bestCount && weight <= bestWeight)) return;
+
+ int worstI = -1, worstS = -1, worstD = 0;
+ long iter = active & ~noReqMask;
+ while (iter != 0L) {
+ long bit = iter & -iter; iter ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (reqFlat[b] > 0) {
+ int d = reqFlat[b] + bonusFlat[b] - T[0];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 0; }
+ }
+ if (reqFlat[b + 1] > 0) {
+ int d = reqFlat[b + 1] + bonusFlat[b + 1] - T[1];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 1; }
+ }
+ if (reqFlat[b + 2] > 0) {
+ int d = reqFlat[b + 2] + bonusFlat[b + 2] - T[2];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 2; }
+ }
+ if (reqFlat[b + 3] > 0) {
+ int d = reqFlat[b + 3] + bonusFlat[b + 3] - T[3];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 3; }
+ }
+ if (reqFlat[b + 4] > 0) {
+ int d = reqFlat[b + 4] + bonusFlat[b + 4] - T[4];
+ if (d > worstD) { worstD = d; worstI = i; worstS = 4; }
+ }
+ }
+
+ if (worstI != -1) {
+ int bc = 0;
+ branchOrder[bc++] = worstI;
+ long negs = active & negMask[worstS] & ~(1L << worstI);
+ while (negs != 0L) {
+ long bit = negs & -negs; negs ^= bit;
+ branchOrder[bc++] = Long.numberOfTrailingZeros(bit);
+ }
+ for (int a = 1; a < bc; a++) {
+ int key = branchOrder[a];
+ int j = a - 1;
+ while (j >= 0 && weights[branchOrder[j]] > weights[key]) {
+ branchOrder[j + 1] = branchOrder[j];
+ j--;
+ }
+ branchOrder[j + 1] = key;
+ }
+ for (int idx = 0; idx < bc; idx++) {
+ tryRemove(active, count, weight, branchOrder[idx]);
+ }
+ return;
+ }
+
+ int os0 = manual[0], os1 = manual[1], os2 = manual[2],
+ os3 = manual[3], os4 = manual[4];
+ long nra = active & noReqMask;
+ while (nra != 0L) {
+ long bit = nra & -nra; nra ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (bonusFlat[b] > 0) os0 += bonusFlat[b];
+ if (bonusFlat[b + 1] > 0) os1 += bonusFlat[b + 1];
+ if (bonusFlat[b + 2] > 0) os2 += bonusFlat[b + 2];
+ if (bonusFlat[b + 3] > 0) os3 += bonusFlat[b + 3];
+ if (bonusFlat[b + 4] > 0) os4 += bonusFlat[b + 4];
+ }
+
+ long remaining = active & ~noReqMask;
+ boolean progress = true;
+ while (progress && remaining != 0L) {
+ progress = false;
+ long ri2 = remaining;
+ while (ri2 != 0L) {
+ long bit = ri2 & -ri2; ri2 ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if ((reqFlat[b] <= 0 || os0 >= reqFlat[b]) &&
+ (reqFlat[b + 1] <= 0 || os1 >= reqFlat[b + 1]) &&
+ (reqFlat[b + 2] <= 0 || os2 >= reqFlat[b + 2]) &&
+ (reqFlat[b + 3] <= 0 || os3 >= reqFlat[b + 3]) &&
+ (reqFlat[b + 4] <= 0 || os4 >= reqFlat[b + 4])) {
+ os0 += bonusFlat[b];
+ os1 += bonusFlat[b + 1];
+ os2 += bonusFlat[b + 2];
+ os3 += bonusFlat[b + 3];
+ os4 += bonusFlat[b + 4];
+ remaining ^= bit;
+ progress = true;
+ }
+ }
+ }
+
+ if (remaining == 0L) {
+ bestCount = count;
+ bestWeight = weight;
+ bestMask = active;
+ return;
+ }
+
+ int stuckI = -1, stuckS = -1, stuckD = 0;
+ long si = remaining;
+ while (si != 0L) {
+ long bit = si & -si; si ^= bit;
+ int i = Long.numberOfTrailingZeros(bit);
+ int b = i * SK;
+ if (reqFlat[b] > 0) {
+ int d = reqFlat[b] - os0;
+ if (d > stuckD) { stuckD = d; stuckI = i; stuckS = 0; }
+ }
+ if (reqFlat[b + 1] > 0) {
+ int d = reqFlat[b + 1] - os1;
+ if (d > stuckD) { stuckD = d; stuckI = i; stuckS = 1; }
+ }
+ if (reqFlat[b + 2] > 0) {
+ int d = reqFlat[b + 2] - os2;
+ if (d > stuckD) { stuckD = d; stuckI = i; stuckS = 2; }
+ }
+ if (reqFlat[b + 3] > 0) {
+ int d = reqFlat[b + 3] - os3;
+ if (d > stuckD) { stuckD = d; stuckI = i; stuckS = 3; }
+ }
+ if (reqFlat[b + 4] > 0) {
+ int d = reqFlat[b + 4] - os4;
+ if (d > stuckD) { stuckD = d; stuckI = i; stuckS = 4; }
+ }
+ }
+
+ if (stuckI == -1) {
+ bestCount = count; bestWeight = weight; bestMask = active;
+ return;
+ }
+
+ int bc = 0;
+ branchOrder[bc++] = stuckI;
+ long negs = active & negMask[stuckS] & ~(1L << stuckI);
+ while (negs != 0L) {
+ long bit = negs & -negs; negs ^= bit;
+ branchOrder[bc++] = Long.numberOfTrailingZeros(bit);
+ }
+ for (int a = 1; a < bc; a++) {
+ int key = branchOrder[a];
+ int j = a - 1;
+ while (j >= 0 && weights[branchOrder[j]] > weights[key]) {
+ branchOrder[j + 1] = branchOrder[j];
+ j--;
+ }
+ branchOrder[j + 1] = key;
+ }
+ for (int idx = 0; idx < bc; idx++) {
+ tryRemove(active, count, weight, branchOrder[idx]);
+ }
+ }
+
+ private void tryRemove(long active, int count, int weight, int i) {
+ int nextCount = count - 1;
+ int nextWeight = weight - weights[i];
+ if (nextCount < bestCount || (nextCount == bestCount && nextWeight <= bestWeight)) return;
+
+ long next = active ^ (1L << i);
+
+ if (next != 0L) {
+ if (useVisited) {
+ int idx = (int) next;
+ int word = idx >>> 6;
+ long b = 1L << (idx & 63);
+ if ((visited[word] & b) != 0L) return;
+ visited[word] |= b;
+ }
+ }
+
+ int base = i * SK;
+ T[0] -= bonusFlat[base];
+ T[1] -= bonusFlat[base + 1];
+ T[2] -= bonusFlat[base + 2];
+ T[3] -= bonusFlat[base + 3];
+ T[4] -= bonusFlat[base + 4];
+
+ dfs(next, nextCount, nextWeight);
+
+ T[0] += bonusFlat[base];
+ T[1] += bonusFlat[base + 1];
+ T[2] += bonusFlat[base + 2];
+ T[3] += bonusFlat[base + 3];
+ T[4] += bonusFlat[base + 4];
+ }
+}