Skip to content

Commit 5228914

Browse files
committed
Merge branch 'dualFixing' of https://github.com/fwesselm/HiGHS into dualBoundTightening
2 parents 8db2116 + c3588b1 commit 5228914

File tree

4 files changed

+244
-29
lines changed

4 files changed

+244
-29
lines changed

examples/call_highs_from_c.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ void full_api() {
652652
assert(dual_solution_status == kHighsSolutionStatusNone);
653653
// The basis status value should indicate invalidity
654654
assert(basis_validity == kHighsBasisValidityInvalid);
655-
assert(mip_node_count == 0);
655+
assert(mip_node_count == 1);
656656

657657
// Get the primal and dual solution
658658
Highs_getSolution(highs, col_value, col_dual, row_value, row_dual);

examples/call_highs_from_c_minimal.c

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ void full_api() {
627627
assert(dual_solution_status == kHighsSolutionStatusNone);
628628
// The basis status value should indicate invalidity
629629
assert(basis_validity == kHighsBasisValidityInvalid);
630-
assert(mip_node_count == 0);
630+
assert(mip_node_count == 1);
631631

632632
// Get the primal and dual solution
633633
Highs_getSolution(highs, col_value, col_dual, row_value, row_dual);

highs/presolve/HPresolve.cpp

Lines changed: 241 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,14 +3232,14 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack,
32323232
HPRESOLVE_CHECKED_CALL(detectDominatedCol(postsolve_stack, col, false));
32333233
if (colDeleted[col]) return Result::kOk;
32343234

3235-
if (mipsolver != nullptr) {
3236-
// check if variable is implied integer
3235+
// check if variable is implied integer
3236+
if (mipsolver != nullptr)
32373237
HPRESOLVE_CHECKED_CALL(
32383238
static_cast<Result>(convertImpliedInteger(col, row)));
32393239

3240-
// try tightening bounds
3241-
dualBoundTightening(postsolve_stack, col);
3242-
}
3240+
// dual fixing
3241+
HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col));
3242+
if (colDeleted[col]) return Result::kOk;
32433243

32443244
// update column implied bounds
32453245
updateColImpliedBounds(row, col, colCoef);
@@ -4379,9 +4379,6 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43794379
// check if variable is implied integer
43804380
HPRESOLVE_CHECKED_CALL(static_cast<Result>(convertImpliedInteger(col)));
43814381

4382-
// try tightening bounds
4383-
dualBoundTightening(postsolve_stack, col);
4384-
43854382
// shift integral variables to have a lower bound of zero
43864383
if (model->integrality_[col] != HighsVarType::kContinuous &&
43874384
model->col_lower_[col] != 0.0 &&
@@ -4398,12 +4395,15 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43984395
transformColumn(postsolve_stack, col, -1.0, model->col_upper_[col]);
43994396
}
44004397
}
4401-
4402-
if (model->integrality_[col] == HighsVarType::kInteger) return Result::kOk;
44034398
}
44044399

4400+
// dual fixing
4401+
HPRESOLVE_CHECKED_CALL(dualFixing(postsolve_stack, col));
4402+
if (colDeleted[col]) return Result::kOk;
4403+
44054404
// update dual implied bounds of all rows in given column
4406-
updateRowDualImpliedBounds(col);
4405+
if (model->integrality_[col] != HighsVarType::kInteger)
4406+
updateRowDualImpliedBounds(col);
44074407

44084408
return Result::kOk;
44094409
}
@@ -4538,19 +4538,190 @@ HPresolve::Result HPresolve::detectDominatedCol(
45384538
return Result::kOk;
45394539
}
45404540

4541-
void HPresolve::dualBoundTightening(HighsPostsolveStack& postsolve_stack,
4542-
HighsInt col) {
4543-
// tighten bounds using dual arguments
4541+
HPresolve::Result HPresolve::dualFixing(HighsPostsolveStack& postsolve_stack,
4542+
HighsInt col) {
4543+
// fix variables or tighten bounds using dual arguments
45444544
// see section 4.4 "Dual fixing, substitution and bound strengthening",
45454545
// Achterberg et al., Presolve Reductions in Mixed Integer Programming,
45464546
// INFORMS Journal on Computing 32(2):473-506.
45474547
assert(!colDeleted[col]);
45484548

4549-
// only tighten bounds for integer-constrained variables
4550-
if (model->integrality_[col] == HighsVarType::kContinuous) return;
4551-
45524549
// return if variable is already fixed
4553-
if (model->col_lower_[col] == model->col_upper_[col]) return;
4550+
if (model->col_lower_[col] == model->col_upper_[col]) return Result::kOk;
4551+
4552+
// struct for storing non-zeros while searching for substitutions
4553+
struct nonZeros {
4554+
HighsInt row;
4555+
double jval;
4556+
double kval;
4557+
};
4558+
std::vector<nonZeros> nzs;
4559+
nzs.reserve(colsize[col]);
4560+
4561+
// lambda for computing locks
4562+
auto computeLocks = [&](HighsInt col, HighsInt& numDownLocks,
4563+
HighsInt& numUpLocks, HighsInt& downLockRow,
4564+
HighsInt& upLockRow) {
4565+
// initialise
4566+
numDownLocks = 0;
4567+
numUpLocks = 0;
4568+
downLockRow = -1;
4569+
upLockRow = -1;
4570+
4571+
// consider objective function
4572+
if (model->col_cost_[col] > 0)
4573+
numUpLocks++;
4574+
else if (model->col_cost_[col] < 0)
4575+
numDownLocks++;
4576+
4577+
// check coefficients
4578+
for (const auto& nz : getColumnVector(col)) {
4579+
// get row index and coefficient
4580+
HighsInt row = nz.index();
4581+
double val = nz.value();
4582+
4583+
// skip redundant rows
4584+
if (isRedundant(row)) continue;
4585+
4586+
// check lhs and rhs for finiteness
4587+
bool lhsFinite = model->row_lower_[row] != -kHighsInf;
4588+
bool rhsFinite = model->row_upper_[row] != kHighsInf;
4589+
4590+
// update number of locks
4591+
if (val > 0) {
4592+
if (lhsFinite) {
4593+
numDownLocks++;
4594+
downLockRow = row;
4595+
}
4596+
if (rhsFinite) {
4597+
numUpLocks++;
4598+
upLockRow = row;
4599+
}
4600+
} else {
4601+
if (lhsFinite) {
4602+
numUpLocks++;
4603+
upLockRow = row;
4604+
}
4605+
if (rhsFinite) {
4606+
numDownLocks++;
4607+
downLockRow = row;
4608+
}
4609+
}
4610+
4611+
// stop early if there are locks in both directions, since the variable
4612+
// cannot be fixed in this case.
4613+
if (numDownLocks > 1 && numUpLocks > 1) break;
4614+
}
4615+
};
4616+
4617+
// lambda for checking whether a row provides an implied bound
4618+
auto hasImpliedBound = [&](HighsInt row, double val) {
4619+
return ((val < 0 && model->row_upper_[row] != kHighsInf) ||
4620+
(val > 0 && model->row_lower_[row] != -kHighsInf));
4621+
};
4622+
4623+
// lambda for variable substitution
4624+
auto substituteCol = [&](HighsInt col, HighsInt row, HighsInt direction,
4625+
double colBound, double otherColBound) {
4626+
// check lhs and rhs for finiteness
4627+
bool lhsFinite = model->row_lower_[row] != -kHighsInf;
4628+
bool rhsFinite = model->row_upper_[row] != kHighsInf;
4629+
4630+
// use storeRow and getStoredRow since getRowVector's rowroot[row] would be
4631+
// overwritten by subsequent findNonZero calls, which would produce
4632+
// undefined behavior
4633+
storeRow(row);
4634+
for (const auto& rowNz : getStoredRow()) {
4635+
// skip column index that was passed to this lambda
4636+
if (rowNz.index() == col) continue;
4637+
4638+
// only consider non-fixed binary variables
4639+
if (model->integrality_[rowNz.index()] != HighsVarType::kInteger ||
4640+
model->col_lower_[rowNz.index()] != 0.0 ||
4641+
model->col_upper_[rowNz.index()] != 1.0)
4642+
continue;
4643+
4644+
// skip binary variable if setting it to its lower bound does not make the
4645+
// row redundant
4646+
if ((rhsFinite && impliedRowBounds.getResidualSumUpperOrig(
4647+
row, rowNz.index(), rowNz.value()) >
4648+
model->row_upper_[row] + primal_feastol) ||
4649+
(lhsFinite && impliedRowBounds.getResidualSumLowerOrig(
4650+
row, rowNz.index(), rowNz.value()) <
4651+
model->row_lower_[row] - primal_feastol))
4652+
continue;
4653+
4654+
// now compute the implied lower bound (direction = 1) or implied upper
4655+
// bound (direction = -1) provided that the binary variable is set to its
4656+
// upper bound. store triplets (row, nonzero, nonzero) in a vector to
4657+
// speed up search
4658+
nzs.clear();
4659+
if (colsize[col] < colsize[rowNz.index()]) {
4660+
for (const auto& colNz : getColumnVector(col)) {
4661+
// skip non-zeros that do not yield an implied bound
4662+
if (!hasImpliedBound(colNz.index(), direction * colNz.value()))
4663+
continue;
4664+
HighsInt nzPos = findNonzero(colNz.index(), rowNz.index());
4665+
if (nzPos == -1) continue;
4666+
nzs.push_back({colNz.index(), colNz.value(), Avalue[nzPos]});
4667+
}
4668+
} else {
4669+
for (const auto& colNz : getColumnVector(rowNz.index())) {
4670+
HighsInt nzPos = findNonzero(colNz.index(), col);
4671+
if (nzPos == -1) continue;
4672+
// skip non-zeros that do not yield an implied bound
4673+
if (!hasImpliedBound(colNz.index(), direction * Avalue[nzPos]))
4674+
continue;
4675+
nzs.push_back({colNz.index(), Avalue[nzPos], colNz.value()});
4676+
}
4677+
}
4678+
4679+
// find best bound
4680+
double bestBound = -kHighsInf;
4681+
for (const auto& triplet : nzs) {
4682+
// compute implied bound from row given that the binary variable is at
4683+
// its upper bound
4684+
double rhs = 0.0;
4685+
double residual = 0.0;
4686+
if (direction * triplet.jval < 0) {
4687+
rhs = model->row_upper_[triplet.row];
4688+
residual = impliedRowBounds.getResidualSumLowerOrig(triplet.row, col,
4689+
triplet.jval) +
4690+
std::max(triplet.kval, 0.0);
4691+
} else {
4692+
rhs = model->row_lower_[triplet.row];
4693+
residual = impliedRowBounds.getResidualSumUpperOrig(triplet.row, col,
4694+
triplet.jval) +
4695+
std::min(triplet.kval, 0.0);
4696+
}
4697+
// direction = 1: compute implied lower bound
4698+
// direction = -1: compute implied upper bound
4699+
double candidateBound = direction * (rhs - residual) / triplet.jval;
4700+
// remember best bound (note the sign switch for direction < 0 above)
4701+
bestBound = std::max(bestBound, candidateBound);
4702+
}
4703+
4704+
// round bound
4705+
if (model->integrality_[col] != HighsVarType::kContinuous)
4706+
bestBound = std::ceil(bestBound - primal_feastol);
4707+
4708+
// check if lower / upper bound is implied
4709+
if (bestBound >= direction * colBound - primal_feastol) {
4710+
// substitute variable
4711+
double offset = otherColBound;
4712+
double scale = colBound - otherColBound;
4713+
postsolve_stack.doubletonEquation(
4714+
-1, col, rowNz.index(), 1.0, -scale, offset, model->col_lower_[col],
4715+
model->col_upper_[col], 0.0, false, false,
4716+
HighsPostsolveStack::RowType::kEq, HighsEmptySlice());
4717+
markColDeleted(col);
4718+
substitute(col, rowNz.index(), offset, scale);
4719+
HPRESOLVE_CHECKED_CALL(checkLimits(postsolve_stack));
4720+
break;
4721+
}
4722+
}
4723+
return Result::kOk;
4724+
};
45544725

45554726
// lambda for computing tighter bounds
45564727
auto hasTighterBound = [&](HighsInt col, HighsInt direction,
@@ -4621,15 +4792,59 @@ void HPresolve::dualBoundTightening(HighsPostsolveStack& postsolve_stack,
46214792
return true;
46224793
};
46234794

4624-
double newBound = 0.0;
4625-
if (hasTighterBound(col, HighsInt{1}, model->col_upper_[col], newBound)) {
4626-
// update upper bound
4627-
changeColUpper(col, std::max(newBound, model->col_lower_[col]));
4628-
} else if (hasTighterBound(col, HighsInt{-1}, model->col_lower_[col],
4629-
newBound)) {
4630-
// update lower bound
4631-
changeColLower(col, std::min(newBound, model->col_upper_[col]));
4795+
// compute locks
4796+
HighsInt numDownLocks = 0;
4797+
HighsInt numUpLocks = 0;
4798+
HighsInt downLockRow = -1;
4799+
HighsInt upLockRow = -1;
4800+
computeLocks(col, numDownLocks, numUpLocks, downLockRow, upLockRow);
4801+
4802+
// check if variable can be fixed
4803+
if (numDownLocks == 0 || numUpLocks == 0) {
4804+
// fix variable
4805+
if (numDownLocks == 0 ? fixColToLowerOrUnbounded(postsolve_stack, col)
4806+
: fixColToUpperOrUnbounded(postsolve_stack, col)) {
4807+
// handle unboundedness
4808+
presolve_status_ = HighsPresolveStatus::kUnboundedOrInfeasible;
4809+
return Result::kDualInfeasible;
4810+
}
4811+
} else {
4812+
if (mipsolver != nullptr && model->col_lower_[col] != -kHighsInf &&
4813+
model->col_upper_[col] != kHighsInf) {
4814+
// try substitution
4815+
if (numDownLocks == 1 && downLockRow != -1) {
4816+
HPRESOLVE_CHECKED_CALL(substituteCol(col, downLockRow, HighsInt{1},
4817+
model->col_upper_[col],
4818+
model->col_lower_[col]));
4819+
} else if (numUpLocks == 1 && upLockRow != -1) {
4820+
HPRESOLVE_CHECKED_CALL(substituteCol(col, upLockRow, HighsInt{-1},
4821+
model->col_lower_[col],
4822+
model->col_upper_[col]));
4823+
}
4824+
if (colDeleted[col]) return Result::kOk;
4825+
}
4826+
// try to strengthen bounds
4827+
double newBound = 0.0;
4828+
if (hasTighterBound(col, HighsInt{1}, model->col_upper_[col], newBound)) {
4829+
// do not make bounds inconsistent
4830+
newBound = std::max(newBound, model->col_lower_[col]);
4831+
// update upper bound
4832+
// only modify bounds on continuous variables if it leads to fixing
4833+
if (model->integrality_[col] != HighsVarType::kContinuous ||
4834+
newBound == model->col_lower_[col])
4835+
changeColUpper(col, newBound);
4836+
} else if (hasTighterBound(col, HighsInt{-1}, model->col_lower_[col],
4837+
newBound)) {
4838+
// do not make bounds inconsistent
4839+
newBound = std::min(newBound, model->col_upper_[col]);
4840+
// update lower bound
4841+
// only modify bounds on continuous variables if it leads to fixing
4842+
if (model->integrality_[col] != HighsVarType::kContinuous ||
4843+
newBound == model->col_upper_[col])
4844+
changeColLower(col, newBound);
4845+
}
46324846
}
4847+
return Result::kOk;
46334848
}
46344849

46354850
HPresolve::Result HPresolve::initialRowAndColPresolve(

highs/presolve/HPresolve.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -379,7 +379,7 @@ class HPresolve {
379379
Result detectDominatedCol(HighsPostsolveStack& postsolve_stack, HighsInt col,
380380
bool handleSingletonRows = true);
381381

382-
void dualBoundTightening(HighsPostsolveStack& postsolve_stack, HighsInt col);
382+
Result dualFixing(HighsPostsolveStack& postsolve_stack, HighsInt col);
383383

384384
Result initialRowAndColPresolve(HighsPostsolveStack& postsolve_stack);
385385

0 commit comments

Comments
 (0)