这是一个半自动的计算程序,这要求您必须稍懂一点Java编程的知识!不过不用担心,只需会调用其中的【方法/函数】即可,而且我已经在README.md文档中写好了使用示例。您几乎只用将文件路径写入使用示例中的参数即可
最后,这个项目被我部署在Gitee仓库里了,您可免费从中拿取完整代码以及使用实例
Element
public class Element implements Comparable<Element>
{
final boolean minus; // 这种设计支持分数
final int son; // ConstPool中有常见的Element缓存(类似Integer缓存了从-128到127之间的整数)
final int mom; // 这是不可变的设计思想,每次计算都会生成新对象
public Element(int son, int mom)
{// 这一块是Element的构造函数
}
public Element(int son, int mom, boolean minus)
{// 这一块是Element的构造函数
}
public Element(int son)
{// 这一块是Element的构造函数
}
public Element(String fraction)
{// 这一块是将字符串解析为Element的构造函数(支持分数,不支持小数)
}
public Element add(Element e)
{
// 这一块是Element之间的加法运算,此处不再赘述,Gitee仓库内有完整代码
return check(s, m, e.minus && minus);
}
public Element sub(Element e)
{
// 这一块是Element之间的减法运算,此处不再赘述,Gitee仓库内有完整代码
return check(s, m, minus);
}
public Element mul(Element e)
{
int s = toIntExact((long) son * e.son);
int m = toIntExact((long) mom * e.mom);
return check(s, m, e.minus ^ minus);
}
public Element div(Element e)
{
int s = toIntExact((long) son * e.mom);
int m = toIntExact((long) mom * e.son);
return check(s, m, e.minus ^ minus);
}
@Override
public int compareTo(Element e)
{
if (e.minus ^ minus)
{
if (e.minus)
return 1;
else
return -1;
}
int s0 = son * e.mom;
int s1 = e.son * mom;
return s0 - s1;
}
@Override
public boolean equals(Object o)
{
if (this == o)
return true;
if (!(o instanceof Element e))
return false;
return minus == e.minus && son == e.son && mom == e.mom;
}
@Override
public String toString()
{
if (ema.equals(this))
return "+INF";
if (emi.equals(this))
return "-INF";
String result = minus ? "-" : "+";
result += mom == 1 ? son + "" : son + "/" + mom;
return result;
}
private Element check(int son, int mom, boolean minus)
{
if (son == 0)
return e0; // ConstantPool的静态常量
if (mom == 0)
throw new IllegalArgumentException("分母不可为零");
if ((son | mom) < 0)
{
minus = true;
son = abs(son);
mom = abs(mom);
}
int gcd = gcd(son, mom); // 尝试将分子分母之间约分
son /= gcd;
mom /= gcd;
return new Element(son, mom, minus);
}
}
InteractUtils
public final class InteractUtils // 这是一个关于交互的代码工具类
{
// 根据name加载一个存放了单纯形表的文件
// 首先去resources文件夹下面找,如果没有找到,则根据name去找
// 即name可以仅为文件名,也可为包含全路径的文件名
public static Table loadTable(String name)
{
InputStream stream = Main.class.getClassLoader().getResourceAsStream(name);
if (stream!=null)
return loadTable(stream);
try
{
return loadTable(new FileInputStream(name));
}
catch (IOException e)
{
throw new IllegalArgumentException("未找到该文件:"+name);
}
}
public static Table loadTable(InputStream stream)
{
Scanner input = new Scanner(stream);
String ax_in = input.next();
if (!"max".equalsIgnoreCase(ax_in) && !"min".equalsIgnoreCase(ax_in))
throw new IllegalArgumentException("单纯形表格式异常");
boolean isMax = "max".equalsIgnoreCase(ax_in);
final int row = input.nextInt(), col = input.nextInt();
Table table = new Table(isMax, row, col);
input.nextLine();
String[] C = input.nextLine().split(" ");
for (int i = 0; i < C.length; i++)
table.setC(i, new Element(C[i]));
for (int i = 0; i < row; i++)
{
for (int j = 0; j < col + 2; j++)
{
if (j == col)
table.setS(i, Symbol.valueOf(input.next().toUpperCase()));
else
{
Element e = new Element(input.next());
if (j == col + 1)
table.setB(i, e);
else
table.setM(i, j, e);
}
}
}
for (int i = 0; i < col; i++)
table.setR(i, Symbol.valueOf(input.next().toUpperCase()));
return table;
}
}
Type
//单纯形表的计算结果可能有四种形式
public enum Type
{ // 唯一解,无穷多最优解,无界解,无可行解
NON, UNI, INF, UNB, UNS; // 尚未计算,unique,infinity,unbounded,unsolvable
@Override
public String toString()
{
String result = null;
switch (this)
{
case NON -> result = "尚未计算";
case UNI -> result = "唯一最优解";
case INF -> result = "无穷多最优解";
case UNB -> result = "无界解";
case UNS -> result = "不可解";
default -> throw new IllegalStateException("Unexpected value: " + this);
}
return result;
}
}
Table(最为繁琐,最为复杂的类!!!)
/**
* @ClassName: Table
* @author: Mr.Peng
* @create: 2023-04-12 16:27
**/
public class Table implements Iterator<Table>
{
public Table(boolean isMax, int row, int col)
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
public Table(boolean isMax, int row, int col, CyclicStrategy strategy)
{
this(isMax, row, col);
this.strategy = strategy.clone();
}
private Table(Table table) // 深克隆
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private Table(boolean isMax, Matrix m) //用于对偶单纯形表
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
@Override
public boolean hasNext()
{
if (!isInit)
throw new OperateException("迭代前须先完成初始化");
if (!strategy.hasNext()) // 若迭代次数过多,则返回false
return false;
computeCheckNum();
for (CheckNum e : this.n)
{
if (CalculateUtils.compare(e, n0).equals(GT))
return true;
}
for (int i = 0; i < row; i++)
{
int idx = x.get(i);
if (c[idx].equals(emi) || c[idx].equals(ema))
{
type = Type.UNS;
throw new ResultException(Type.UNS); //即该问题不可解
}
}
for (int i = 0; i < col; i++)
{
if (x.contains(i)) // 跳过基变量
continue;
if (n[i].equals(n0))
{
type = Type.INF; // 判断是否为无穷多最优解
break;
}
}
if (!type.equals(Type.INF))
type = Type.UNI;
return false;
}
@Override
public Table next()
{
if (!isInit)
throw new OperateException("迭代前须先完成初始化");
Table table = new Table(this);
table.strategy.next();
int k = table.findIn(); // k:入基变量
int l = table.findOut(k); // l:出基变量
table.handleBaseVar(k, l);
table.transformation(l, k);
return table;
}
private void transformation(int out, int in) // 入基变量替换出基变量时的初等变换
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
@Override
public String toString()
{
StringBuilder builder = new StringBuilder((isMax ? "max" : "min") + "=");
String temp = " ".repeat(builder.length());
for (Element e : c)
format(e, builder);
builder.append(";\n");
Element[][] es = m.es;
for (int i = 0; i < es.length; i++)
{
builder.append(temp);
for (int j = 0; j < es[i].length; j++)
format(es[i][j], builder);
builder.append("\t").append(s[i]).append("\t").append(b[i]).append(";\n");
}
builder.append(temp);
for (int i = 0; i < col; i++)
builder.append(" ".repeat(FORMAT_LENGTH - 2)).append(r[i]);
builder.append("\n").append(temp);
for (int i = 0; i < col; i++)
format(n[i], builder);
return builder.toString();
}
private Table getTable()
{
Table table = null;
if (!isInit)
table = standard();
else
table = new Table(this);
return table;
}
public Result result()
{
Table table = getTable();
try
{
while (table.hasNext())
table = table.next();
Element r = e0;
for (int i = 0; i < table.row; i++)
{
int baseVar = table.x.get(i);
r = r.add(table.b[i].mul(table.c[baseVar]));
}
return new Result(r,table.type);
}
catch (ResultException re)
{
return new Result(null, table.type);
}
}
private void computeCheckNum() // 计算检验数
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
public int findIn() // 普通单纯形表中,先找到入基变量,再找出基变量
{
int idx = 0;
for (int i = 0; i < n.length; i++)
{
if (CalculateUtils.compare(n[i], n[idx]) == GT)
idx = i;
}
return idx;
}
public int findOut(int k) // 普通单纯形表中,先找到入基变量,再找出基变量
{
int idx = -1;
Element temp = null;
for (int r = 0; r < row; r++)
{
if (m.es[r][k].minus || !CalculateUtils.compare(m.es[r][k], e0).equals(GT))
continue;
Element div = b[r].div(m.es[r][k]);
if (idx == -1 || CalculateUtils.compare(temp, div) == GT)
{
temp = div;
idx = r;
}
}
if (idx == -1)
{
type = Type.UNB;
throw new ResultException(Type.UNB); // 即该问题有无界解}
}
return idx;
}
public void setC(int idx, Element e) {this.c[idx] = e;}
public void setB(int idx, Element e) {this.b[idx] = e;}
public void setM(int row, int col, Element e) {this.m.es[row][col] = e;
public void setS(int idx, Symbol symbol) {this.s[idx] = symbol;}
public void setR(int idx, Symbol restrain) {this.r[idx] = restrain;}
private void checkS()
{
for (int i = 0; i < this.s.length; i++)
{
if (EQ.equals(this.s[i]))
continue;
switch (this.s[i])
{
case LE -> this.appendC(i, e0, false);
case GE -> this.appendC(i, e0, true);
default -> throw new IllegalStateException("Unexpected value: " + s[i]);
}
}
}
private void checkR() // 对约束条件的标准化
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private void checkC() // 对目标函数的标准化
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private void checkB() // 标准化
{
for (int i = 0; i < row; i++) //检测`b[i]`是否小于0,若成立则对该行全体成员*(-1):此时可能会导致某些`Xj`全为负
{
if (CalculateUtils.compare(e0, this.b[i]) == GT)
{
this.b[i] = this.b[i].mul(_e1);
Element[] eles = this.m.es[i];
for (int j = 0; j < eles.length; j++)
eles[j] = eles[j].mul(_e1);
}
}
}
private void checkM()
{
// 该方法起初是为解决【存在一列`Xj`全为负】,但后来查书得:若全为负且该列检验数>0,则为无界解
}
public Table standard()
{
Table table = new Table(this);
table.checkC(); table.checkS();
table.checkR();table.checkB();table.checkM();
table.handleBaseVar(0, 0); // 确定基矩阵
if (table.x.size() != row) // 化为标准形之后,基变量个数不满足
table.customVar();
table.isInit = true; // 表示其已化为基矩阵
return table;
}
private void customVar() // 人工变量法
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private void handleBaseVar(int in, int out) // 处理入基变量与出基变量
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
public Table adjust() // 进行列的交换,以使其基变量聚在一起(更好看)
{
Table table = getTable();
for (int i = 0; i < row; i++)
{
int idx = table.x.get(i);
if (idx != i)
{
swap(table.c, i, idx);
swap(table.m.es, Select.COL, i, idx);
table.x.update(i, idx);
}
}
return table;
}
private void appendC(int idx) // 用于标准化中的加一列
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private void appendC(int row, Element c, boolean minus) // 用于标准化中的加一列
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
public Table dual() // 转为单纯形表
{//代码过长,此处不再赘述,完整版在Gitee仓库里
}
private Type type;
private boolean isInit;
private boolean isMax;
private BaseVar x; // 基变量
private int row;
private int col;
private Symbol[] s; // 原式符号(化为标准形时,一律为等于)
private Matrix m;
private Element[] c; // 目标函数
private Element[] b;
private CheckNum[] n; // 检验数:checkNum
private Symbol[] r; // restrains:该变量是否
private CyclicStrategy strategy;// 这算是一个策略模式,为了解决迭代次数问题(前期开发时,迭代流程不稳定,有时会陷入死循环,所以通过这个方式来解决此问题)
}
如何计算可逆矩阵
所有会用到的数学公式如下
问题拆分
计算过程中,最耗时最繁琐的地方就是计算行列式了。它需要n个数的全排列,这又是一个耗时严重的地方——列举过程的时间复杂度是【O(N * N!)】。所以这里我采用多线程的方式来计算,并通过阻塞队列来进行线程间的通信。
又因为再计算一个可逆矩阵的问题时,要计算多个规格相同的行列式,所以我又将全排列的列举结果缓存起来(这里我借用了hutool.cache.LRU作为我的缓存管理类)
排列数的计算以及多线程的计算
public static Permutation permutation(final int length)
{
if (!cache.containsKey(length))
{
Permutation p = new Permutation(length);
cache.put(length, p);
local.put(length, new ArrayBlockingQueue<>(p.accumulation));
// local.put(length, new LinkedBlockingQueue<>());
threadPool.execute(() -> // 通过线程池来控制线程数量
{
int[] nums = new int[length];
for (int i = 0; i < length; i++)
nums[i] = i;
backtrack(0, nums, length);
});
}
return cache.get(length);
}
public static int[] nextA(int idx,int length)
{
Permutation p = cache.get(length);
if (p.handle[idx]) // 若已拿到相关数据
return p.arrays[idx];
else
{
p.arrays[idx] = nextA(length);
p.handle[idx] = true;
return p.arrays[idx];
}
}
private static int[] nextA(int length) // 从阻塞队列里拿到计算线程的数据(线程间通信)
{
try
{
return local.get(length).take();
}
catch (Exception e)
{
e.printStackTrace();
throw new RuntimeException();
}
}
//计算排列数
private static void backtrack(int idx, int[] nums, final int id)
{
if (idx == nums.length)
{
try
{
int[] result = new int[nums.length];
System.arraycopy(nums, 0, result, 0, nums.length);
local.get(id).put(result); // 向这个规格里的阻塞队列里放数据
}
catch (InterruptedException e)
{
e.printStackTrace();
}
return;
}
for (int i = idx; i < nums.length; ++i)
{
swap(nums, i, idx); // 交换数组中i与idx的元素
backtrack(idx + 1, nums, id);
swap(nums, i, idx);
}
}
//计算逆序数
public static int inverseNum(int[] nums)
{
int num = 0;
for (int i = 0; i < nums.length; i++)
num += DFS(i, i, nums);
return num;
}
private static int DFS(int idx, int key, int[] nums)
{
if (idx == nums.length)
return 0;
int temp = nums[idx] < nums[key] ? 1 : 0;
return DFS(idx + 1, key, nums) + temp;
}
可逆矩阵的计算
public Matrix invert() // 可逆矩阵
{
if (es.length != es[0].length)
throw new OperateException("该矩阵不可计算逆矩阵");
Element calculate = determinant();
if (e0.equals(calculate))
throw new OperateException("方阵不可逆");
Matrix matrix = new Matrix(es.length, es.length);
Element[][] es = matrix.es;
for (int i = 0; i < es.length; i++)
{
for (int j = 0; j < this.es[i].length; j++)
es[i][j] = complementMinor(i, j);
}
return matrix.div(calculate).transposition();
}
private Element complementMinor(int ii, int jj) // 代数余子式
{
if (es.length != es[0].length)
throw new OperateException("该矩阵不可计算代数余子式");
boolean minus = ((ii ^ jj) & 1) == 1;
Matrix matrix = new Matrix(es.length - 1);
boolean flagI = false;
for (int i = 0; i < es.length; i++)
{
if (i == ii)
{
flagI = true;
continue;
}
boolean flagJ = false;
for (int j = 0; j < es[i].length; j++)
{
if (j == jj)
{
flagJ = true;
continue;
}
int I = flagI ? i - 1 : i;
int J = flagJ ? j - 1 : j;
matrix.es[I][J] = es[i][j];
}
}
return minus ? _e1.mul(matrix.determinant()) : e_1.mul(matrix.determinant());
}
public Element determinant() // 系数行列式
{
if (es.length != es[0].length)
throw new OperateException("该矩阵不可计算系数行列式");
if (es.length>14)
throw new IllegalArgumentException("该方阵规模较大,不可计算行列式");
Permutation permutation = CalculateUtils.permutation(es.length);
Element result = e0;
for (int i = 0; i < permutation.accumulation; i++)
{
Element r = e_1;
int[] ints = CalculateUtils.nextA(i,permutation.num);
for (int j = 0; j < ints.length; j++)
{
r = r.mul(es[j][ints[j]]);
if (e0.equals(r))
break;
}
if (e0.equals(r))
continue;
if ((CalculateUtils.inverseNum(ints) & 1) == 1) //判断该排列的逆序数,以决定其正负性
result = result.sub(r);
else
result = result.add(r);
}
return result;
}
如何计算单纯形表
由于此过程较为复杂,所以不再贴代码,完整版在Gitee仓库
虽然计算单纯形表的程序开发起来较为繁琐,但其实只要牢记基本规则/流程,然后不断拆分就好。任何再复杂的流程都可通过此方式来解决
- 化为标准型
- 确定初始基和可行解
- 计算检验数,并判断是否要迭代
- 找到检验数最大的那一列——确定入基变量
- 从该列找到 b/a 最大的一行——确定出基变量
- 用入基变量替代出基变量
- 找到最优解,并计算最终结果
写在最后
我在大二下学期时就学了运筹学这门课程,第一眼看到单纯形表时就感觉可以用程序来解,但一直拖着没做(眼高手低,又懒又傲)。当时觉得只要把流程看看就好,没什么复杂的,所以直到上考场,也没亲手解决一个单纯形表的问题,所以我没有通过这门课程。
我很早就看过刘慈欣写的《三体》,里面有一句话很触动我——“弱小和无知都不是人类生存的障碍,傲慢才是”。但真正面对问题时,我每一次都会选择逃避,每一次都会选择傲慢,每一次都会选择没必要……然后吃到苦头时,才会感觉那句话写的真好(后人哀之而不鉴之,亦使后人而复哀后人也)。后来补考时,我又傲慢的认为补考很简单,我肯定能过……所以很多时候并不是没有机会,而是给我机会,我傲慢的没有伸手去接。
在写这篇文章时,我已经大三了。从学编程到现在,学了很多底层原理,学了很多程序框架,但一直没去做我想做的东西。做的项目也都是去咀嚼别人吃过的东西(阿里面试官直言我简历上的项目不行),然后用“先看后做”的借口来逃避。关于开发,我疯狂的学了一大堆,但几乎没有实战的校验……
就在前几天,我开始尝试找实习工作,但没有一个中大厂给我面试机会,所以我只好不甘心地来到一个小公司实习,做着不喜欢的工作……有一说一,这完全不符合我的预期,完全不符合我在高考结束后的那个暑假所作的梦。这几天的每一个夜晚,我都会在想:到底是哪一步做错了,我到底应不应该把我如今的现状归结为【市场不好(我学的是Java后端)+经济下行】?
辗转反侧后,我觉得不能再这样了,我应该主动面对。逃避+傲慢之后让这一切变得更糟,积极准备才能走向未来。酌贪泉而觉爽,处涸辙以犹欢,享受无法回避的痛苦才是人生的常态(之前总因社会上的一些负面新闻而觉得上升渠道关闭,底层人就应躺平)。牢记:傲慢才是人类生存的障碍!