Skip to content

Commit

Permalink
add integer programming solver and benchmark test
Browse files Browse the repository at this point in the history
  • Loading branch information
simontao committed Sep 8, 2024
1 parent 13c316f commit 50ff0d0
Show file tree
Hide file tree
Showing 8 changed files with 247 additions and 40 deletions.
1 change: 1 addition & 0 deletions .idea/gradle.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions algorithm/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
plugins {
id("java")
id("me.champeau.jmh") version "0.7.2"

}

group = "dev.carbonshow.algorithm"
Expand All @@ -15,6 +17,9 @@ dependencies {
implementation("org.apache.commons:commons-lang3:3.17.0")
implementation("com.google.ortools:ortools-java:9.10.4067")

jmh("org.openjdk.jmh:jmh-core:1.37")
jmh("org.openjdk.jmh:jmh-generator-annprocess:1.37")
jmh("org.openjdk.jmh:jmh-generator-bytecode:1.37")
testImplementation(platform("org.junit:junit-bom:5.10.0"))
testImplementation("org.junit.jupiter:junit-jupiter")
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package dev.carbonshow.algorithm.partition;

import org.openjdk.jmh.annotations.*;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@State(Scope.Benchmark)
public class MaxPartitionBenchmark {

final private Map<Integer, Integer> ADDENDS = Map.of(1, 100, 2, 40, 5, 10);
final private int PARTITIONED = 10;
final private ArrayList<Map<Integer, Long>> PARTITION_PLANS = new ArrayList<>(Arrays.asList(Map.of(1, 6L, 2, 2L),
Map.of(1, 1L, 2, 2L, 5, 1L),
Map.of(5, 2L),
Map.of(1, 5L, 5, 1L)
));

final private DefaultMaxPartitions solver = new DefaultMaxPartitions();
final private IntegerProgrammingMaxPartitions ipSolver = new IntegerProgrammingMaxPartitions();

@Fork(value = 1, warmups = 1)
@Benchmark
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
public void solveTwoPhase() {
solver.solve(ADDENDS, PARTITIONED);
}

@Fork(value = 1, warmups = 1)
@Benchmark
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
public void solveWithPartitionsTwoPhase() {
solver.solveWithPartitionPlan(ADDENDS, PARTITIONED, PARTITION_PLANS);
}

@Fork(value = 1, warmups = 1)
@Benchmark
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@BenchmarkMode(Mode.AverageTime)
public void solveWithIntegerProgramming() {
ipSolver.solve(ADDENDS, PARTITIONED);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ public class DefaultMaxPartitions implements TwoPhaseMaxPartitions {
* 获取最大划分的细节数据
* @param addends 加数集合,这是一个二维数组,一维表示不同的加数,二维应该具有两个元素,分别为加数的值,以及当前加数的数量
* @param partitioned 对上面的加数集合进行划分,每个划分所含加数之和应该等于该值。
* @return 返回最大划分的细节数据,包含每个划分的组成,以及该划分实例化之后的数据
* @return 返回最大划分的细节数据,包含每个划分的组成,以及该划分实例化之后的数据,不存在则返回 null
*/
@Override
public ArrayList<PartitionData> solve(Map<Integer, Integer> addends, int partitioned) {
var orderedAddends = new int[addends.size()];
var orderedAddendCounts = new int[addends.size()];
addendsToArray(addends, orderedAddends, orderedAddendCounts);
MaxPartitionsUtils.addendsToArray(addends, orderedAddends, orderedAddendCounts);

var partitionPlans = solveIntegerPartition(orderedAddends, partitioned);

Expand All @@ -68,7 +68,7 @@ public ArrayList<PartitionData> solve(Map<Integer, Integer> addends, int partiti
public ArrayList<PartitionData> solveWithPartitionPlan(Map<Integer, Integer> addends, int partitioned, ArrayList<Map<Integer, Long>> partitionPlans) {
var orderedAddends = new int[addends.size()];
var orderedAddendCounts = new int[addends.size()];
addendsToArray(addends, orderedAddends, orderedAddendCounts);
MaxPartitionsUtils.addendsToArray(addends, orderedAddends, orderedAddendCounts);

return solveImpl(orderedAddends, orderedAddendCounts, partitionPlans);
}
Expand All @@ -83,7 +83,7 @@ public ArrayList<PartitionData> solveWithPartitionPlan(Map<Integer, Integer> add
* @param orderedAddends 升序排列的加数数组
* @param orderedAddendCounts 加数对应的数量,和 orderedAddends 中加数出现的顺序一一对应
* @param partitionPlans 划分方案列表,每个划分方案以 Map 形式表示,key 是加数,value 是加数出现次数
* @return 返回划分详细数据列表,可能返回 null
* @return 返回划分详细数据列表,不存在则返回 null
*/
private ArrayList<PartitionData> solveImpl(int[] orderedAddends, int[] orderedAddendCounts, ArrayList<Map<Integer, Long>> partitionPlans) {

Expand Down Expand Up @@ -133,7 +133,7 @@ private ArrayList<Integer> solveIntegerProgramming(int[] addends, int[] addendCo
int addendMaxCount = NumberUtils.max(addendCounts);
ArrayList<MPVariable> partitionPlanCountVariables = new ArrayList<>(partitionPlans.size());
for (int i = 0; i < partitionPlans.size(); i++) {
partitionPlanCountVariables.add(programmingSolver.makeIntVar(0.0, addendMaxCount, "p"+i));
partitionPlanCountVariables.add(programmingSolver.makeIntVar(0, addendMaxCount, "p"+i));
}

// 约束条件,每个加数在所有方案中出现的总次数不得超过其总数,所以约束条件数量和加数相同
Expand All @@ -142,7 +142,7 @@ private ArrayList<Integer> solveIntegerProgramming(int[] addends, int[] addendCo
int addendCountTotal = addendCounts[i];

// 加数的约束条件,使用的总次数一定处于 [0, addendCountTotal] 之内
var constraint = programmingSolver.makeConstraint(0.0, addendCountTotal, "c"+i);
var constraint = programmingSolver.makeConstraint(0, addendCountTotal, "c"+i);

// 决策变量即每个划分方案的实施次数。对于当前加数而言:
// 加数使用总次数 = 方案1实施次数*方案1内加数使用次数 + 方案2实施次数*方案2内加数使用次数 + ...
Expand All @@ -169,21 +169,4 @@ private ArrayList<Integer> solveIntegerProgramming(int[] addends, int[] addendCo
return null;
}
}

/**
* 将 Map 类型的加数按照加数升序方式排列,并将加数和加数的数量分别保存在两个数组中
* @param addends 加数集合
* @param orderedAddends 输出的升序排列的加数
* @param orderedAddendCounts 输出的加数数量,和 orderedAddends 一一对应
*/
private void addendsToArray(Map<Integer, Integer> addends, int[] orderedAddends, int[] orderedAddendCounts) {
// 对加数进行升序排序
var sortedAddendMap = new TreeMap<>(addends);
int i = 0;
for (Integer addend : sortedAddendMap.keySet()) {
orderedAddends[i] = addend;
orderedAddendCounts[i] = sortedAddendMap.get(addend);
i++;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package dev.carbonshow.algorithm.partition;

import com.google.ortools.Loader;
import com.google.ortools.linearsolver.MPObjective;
import com.google.ortools.linearsolver.MPSolver;
import com.google.ortools.linearsolver.MPVariable;
import org.apache.commons.lang3.StringUtils;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
* 基于整数线性规划实现的解决方案,具体是:
* <ul>
* <li>建立二维决策变量 Variables[MAX_ADDEND_KINDS][MAX_PARTITIONS_NUM],表示各加数在各划分方案中使用次数
* <ul>
* <li>MAX_ADDEND_KINDS,表示加数的不同种类的数量</li>
* <li>MAX_PLANS_NUM,表示划分的最大数量,可以划分可以重复,实际上不大于单个加数的最大数量</li>
* </ul>
* </li>
* <li>建立一维决策变量 Variables[MAX_PARTITIONS_NUM],用于记录实际每个方案划分,要么存在是 1,要么不存在是 0</li>
* <li>约束条件有:
* <ul>
* <li>每个划分使用的加数之和必须等于被划分数</li>
* <li>每种加数在所有划分中的实际使用次数不能超过其总数</li>
* </ul>
* </li>
* <li>优化目标是:Variables[MAX_PARTITIONS_NUM] 中各变量之和最大化 </li>
* </ul>
*/
public class IntegerProgrammingMaxPartitions implements MaxPartitions {
// 基于 Google OR-Tools 的数学优化器
final private MPSolver solver;

IntegerProgrammingMaxPartitions() {
Loader.loadNativeLibraries();
solver = MPSolver.createSolver("SCIP");
if (solver == null) {
throw new RuntimeException("fail to create solver for IntegerProgramming");
}
}

/**
* 基于整数线性规划一步完成整个计算过程,不再区分划分方案和划分实例,而是直接评估划分实例:
* <ul>
* <li>决策变量:加数在不同划分中的出现次数;以及每个划分方案是否实际使用的标记 0 或 1</li>
* <li>约束条件:每个划分的加数之和等于被划分数;每种加数的使用次数不超过加数总数</li>
* <li>优化目标:每个划分方案的标记标记之和最大化,即有效划分的数量最大化</li>
* </ul>
*
* @param addends 加数集合,这是一个二维数组,一维表示不同的加数,二维应该具有两个元素,分别为加数的值,以及当前加数的数量
* @param partitioned 对上面的加数集合进行划分,每个划分所含加数之和应该等于该值。
* @return 返回最终划分结果的详细数据,将所有划分方案的组成以及实施数量罗列出来,不存在则返回 null
*/
@Override
public ArrayList<PartitionData> solve(Map<Integer, Integer> addends, int partitioned) {
// 清理求解器
solver.clear();

// 确定加数种类的最大数量和划分的最大数量,划分最大数量一定不超过所有加数数量之和
final int maxAddendKinds = addends.size();
final int maxPartitions = addends.values().stream().mapToInt(i -> i).sum();

// 将加数和对应总数拆分到两个不同的数组中,并按加数大小升序排列
var orderedAddends = new int[addends.size()];
var orderedAddendCounts = new int[addends.size()];
MaxPartitionsUtils.addendsToArray(addends, orderedAddends, orderedAddendCounts);

// 定义加数决策变量, 即每个划分中该加数使用的数量,每个加数决策变量的上限必定不能超过当前加数的总数
MPVariable[][] addendVariables = new MPVariable[maxAddendKinds][maxPartitions];
for (int i = 0; i < maxAddendKinds; i++) {
for (int j = 0; j < maxPartitions; j++) {
addendVariables[i][j] = solver.makeIntVar(0, orderedAddendCounts[i], StringUtils.join("a", i, "_", j));
}
}

// 定义划分决策变量,每个划分要么存在为 1,要么不存在为 0
MPVariable[] partitionVariables = new MPVariable[maxPartitions];
for (int j = 0; j < maxPartitions; j++) {
partitionVariables[j] = solver.makeIntVar(0, 1, "p" + j);
}

// 添加加数的约束条件,即加数使用总数不得超限
for (int i = 0; i < maxAddendKinds; i++) {
var constraint = solver.makeConstraint(0, orderedAddendCounts[i], "ac" + i);
for (int j = 0; j < maxPartitions; j++) {
constraint.setCoefficient(addendVariables[i][j], 1);
}
}

// 添加划分的约束条件,每个划分要么存在要么不存在,但不管怎样加数之和,划分标记与被划分数值乘积,两者必定相等
// 换句话两者差值为 0
for (int j = 0; j < maxPartitions; j++) {
var constraint = solver.makeConstraint(0, 0, "pc" + j);
constraint.setCoefficient(partitionVariables[j], -partitioned);
for (int i = 0; i < maxAddendKinds; i++) {
constraint.setCoefficient(addendVariables[i][j], orderedAddends[i]);
}
}

// 设定优化目标,即方案总数最大化
MPObjective objective = solver.objective();
for (int j = 0; j < maxPartitions; j++) {
objective.setCoefficient(partitionVariables[j], 1);
}
objective.setMaximization();

// 求解
final MPSolver.ResultStatus result = solver.solve();
if (result == MPSolver.ResultStatus.OPTIMAL) {
ArrayList<PartitionData> partitionData = new ArrayList<>();
for (int j = 0; j < maxPartitions; j++) {
if (partitionVariables[j].solutionValue() == 1) {
// 说明该划分实际存在,获取划分内容
var partition = new HashMap<Integer, Long>();
for (int i = 0; i < maxAddendKinds; i++) {
var addendCnt = (long) addendVariables[i][j].solutionValue();
if (addendCnt > 0) {
partition.merge(orderedAddends[i], addendCnt, (k, v) -> v + addendCnt);
}
}
partitionData.add(new PartitionData(partition, 1));
}
}
return partitionData;
}
return null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,9 @@ interface MaxPartitions {
* <li>每个划分中,各加数之和</li>
* </ul>
*
* @param finiteAddends 加数集合,这是一个二维数组,一维表示不同的加数,二维应该具有两个元素,分别为加数的值,以及当前加数的数量
* @param addends 加数集合,这是一个二维数组,一维表示不同的加数,二维应该具有两个元素,分别为加数的值,以及当前加数的数量
* @param partitioned 对上面的加数集合进行划分,每个划分所含加数之和应该等于该值。
* @return 返回最终划分结果的详细数据,将所有划分方案的组成以及实施数量罗列出来
* @return 返回最终划分结果的详细数据,将所有划分方案的组成以及实施数量罗列出来;不存在则返回 null
*/
ArrayList<PartitionData> solve(Map<Integer, Integer> finiteAddends, int partitioned);
ArrayList<PartitionData> solve(Map<Integer, Integer> addends, int partitioned);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package dev.carbonshow.algorithm.partition;

import java.util.Map;
import java.util.TreeMap;

public class MaxPartitionsUtils {

/**
* 将 Map 类型的加数按照加数升序方式排列,并将加数和加数的数量分别保存在两个数组中
*
* @param addends 加数集合
* @param orderedAddends 输出的升序排列的加数
* @param orderedAddendCounts 输出的加数数量,和 orderedAddends 一一对应
*/
static void addendsToArray(Map<Integer, Integer> addends, int[] orderedAddends, int[] orderedAddendCounts) {
// 对加数进行升序排序
var sortedAddendMap = new TreeMap<>(addends);
int i = 0;
for (Integer addend : sortedAddendMap.keySet()) {
orderedAddends[i] = addend;
orderedAddendCounts[i] = sortedAddendMap.get(addend);
i++;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package dev.carbonshow.algorithm.partition;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import java.util.ArrayList;
Expand All @@ -8,7 +9,7 @@

import static org.junit.jupiter.api.Assertions.*;

class DefaultMaxPartitionsTest {
class MaxPartitionsTest {
final private Map<Integer, Integer> ADDENDS = Map.of(1, 100, 2, 40, 5, 10);
final private int PARTITIONED = 10;
final private ArrayList<Map<Integer, Long>> PARTITION_PLANS = new ArrayList<>(Arrays.asList(Map.of(1, 6L, 2, 2L),
Expand All @@ -17,10 +18,35 @@ class DefaultMaxPartitionsTest {
Map.of(1,5L,5,1L)
));

@Tag("TwoPhase")
@Test
void solve() {
DefaultMaxPartitions programmingMaxPartition = new DefaultMaxPartitions();
var partitions = programmingMaxPartition.solve(ADDENDS, PARTITIONED);
void solveTwoPhase() {
DefaultMaxPartitions solver = new DefaultMaxPartitions();
var partitions = solver.solve(ADDENDS, PARTITIONED);
validate(partitions);
}

@Tag("TwoPhase")
@Test
void solveWithPartitionPlanTwoPhase() {
DefaultMaxPartitions solver = new DefaultMaxPartitions();
var partitions = solver.solveWithPartitionPlan(ADDENDS, PARTITIONED, PARTITION_PLANS);
assertNotNull(partitions);
for (var data : partitions) {
System.out.printf("%s\n", data);
}
}

@Tag("IntegerProgramming")
@Test
void solveIntegerProgramming() {
IntegerProgrammingMaxPartitions solver = new IntegerProgrammingMaxPartitions();
var partitions = solver.solve(ADDENDS, PARTITIONED);
validate(partitions);
}

// 划分结果的校验逻辑
private void validate(ArrayList<PartitionData> partitions){
assertNotNull(partitions);
for (var data : partitions) {
System.out.printf("%s\n", data);
Expand All @@ -36,14 +62,4 @@ void solve() {
assertTrue(entry.getValue() <= ADDENDS.get(entry.getKey()));
}
}

@Test
void solveWithPartitionPlan() {
DefaultMaxPartitions programmingMaxPartition = new DefaultMaxPartitions();
var partitions = programmingMaxPartition.solveWithPartitionPlan(ADDENDS, PARTITIONED, PARTITION_PLANS);
assertNotNull(partitions);
for (var data : partitions) {
System.out.printf("%s\n", data);
}
}
}

0 comments on commit 50ff0d0

Please sign in to comment.