@@ -243,6 +243,13 @@ bool HPresolve::isRanged(HighsInt row) const {
243243 model->row_upper_ [row] != kHighsInf );
244244}
245245
246+ bool HPresolve::isRedundant (HighsInt row) const {
247+ return (impliedRowBounds.getSumLower (row) >=
248+ model->row_lower_ [row] - primal_feastol &&
249+ impliedRowBounds.getSumUpper (row) <=
250+ model->row_upper_ [row] + primal_feastol);
251+ }
252+
246253bool HPresolve::isImpliedEquationAtLower (HighsInt row) const {
247254 // if the implied lower bound on a row dual is strictly positive then the row
248255 // is an implied equation (using its lower bound) due to complementary
@@ -3225,12 +3232,19 @@ HPresolve::Result HPresolve::singletonCol(HighsPostsolveStack& postsolve_stack,
32253232 HPRESOLVE_CHECKED_CALL (detectDominatedCol (postsolve_stack, col, false ));
32263233 if (colDeleted[col]) return Result::kOk ;
32273234
3235+ // check if variable is implied integer
32283236 if (mipsolver != nullptr )
32293237 HPRESOLVE_CHECKED_CALL (
32303238 static_cast <Result>(convertImpliedInteger (col, row)));
32313239
3240+ // dual fixing
3241+ HPRESOLVE_CHECKED_CALL (dualFixing (postsolve_stack, col));
3242+ if (colDeleted[col]) return Result::kOk ;
3243+
3244+ // update column implied bounds
32323245 updateColImpliedBounds (row, col, colCoef);
32333246
3247+ // update row dual implied bounds
32343248 if (model->integrality_ [col] != HighsVarType::kInteger )
32353249 updateRowDualImpliedBounds (row, col, colCoef);
32363250
@@ -3300,10 +3314,7 @@ HPresolve::Result HPresolve::rowPresolve(HighsPostsolveStack& postsolve_stack,
33003314 };
33013315
33023316 auto checkRowRedundant = [&](HighsInt row) {
3303- if (impliedRowBounds.getSumLower (row) >=
3304- model->row_lower_ [row] - primal_feastol &&
3305- impliedRowBounds.getSumUpper (row) <=
3306- model->row_upper_ [row] + primal_feastol) {
3317+ if (isRedundant (row)) {
33073318 // row is redundant
33083319 int presolveRule =
33093320 rowsize[row] != 0 ? kPresolveRuleRedundantRow : kPresolveRuleEmptyRow ;
@@ -4365,6 +4376,7 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43654376 isUpperImplied (col),
43664377 impliedDualRowBounds.getNumInfSumLowerOrig (col));
43674378
4379+ // check if variable is implied integer
43684380 HPRESOLVE_CHECKED_CALL (static_cast <Result>(convertImpliedInteger (col)));
43694381
43704382 // shift integral variables to have a lower bound of zero
@@ -4383,12 +4395,15 @@ HPresolve::Result HPresolve::colPresolve(HighsPostsolveStack& postsolve_stack,
43834395 transformColumn (postsolve_stack, col, -1.0 , model->col_upper_ [col]);
43844396 }
43854397 }
4386-
4387- if (model->integrality_ [col] == HighsVarType::kInteger ) return Result::kOk ;
43884398 }
43894399
4400+ // dual fixing
4401+ HPRESOLVE_CHECKED_CALL (dualFixing (postsolve_stack, col));
4402+ if (colDeleted[col]) return Result::kOk ;
4403+
43904404 // update dual implied bounds of all rows in given column
4391- updateRowDualImpliedBounds (col);
4405+ if (model->integrality_ [col] != HighsVarType::kInteger )
4406+ updateRowDualImpliedBounds (col);
43924407
43934408 return Result::kOk ;
43944409}
@@ -4523,6 +4538,296 @@ HPresolve::Result HPresolve::detectDominatedCol(
45234538 return Result::kOk ;
45244539}
45254540
4541+ HPresolve::Result HPresolve::dualFixing (HighsPostsolveStack& postsolve_stack,
4542+ HighsInt col) {
4543+ // fix variables or tighten bounds using dual arguments
4544+ // see section 4.4 "Dual fixing, substitution and bound strengthening",
4545+ // Achterberg et al., Presolve Reductions in Mixed Integer Programming,
4546+ // INFORMS Journal on Computing 32(2):473-506.
4547+ assert (!colDeleted[col]);
4548+
4549+ // return if variable is already fixed
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 checking whether a row provides an implied lower bound
4562+ // (direction = 1) or implied upper bound (direction = -1)
4563+ auto hasImpliedBound = [&](HighsInt row, HighsInt direction, double val) {
4564+ return ((direction * val < 0 && model->row_upper_ [row] != kHighsInf ) ||
4565+ (direction * val > 0 && model->row_lower_ [row] != -kHighsInf ));
4566+ };
4567+
4568+ // lambda for computing locks
4569+ auto computeLocks = [&](HighsInt col, HighsInt& numDownLocks,
4570+ HighsInt& numUpLocks, HighsInt& downLockRow,
4571+ HighsInt& upLockRow) {
4572+ // initialise
4573+ numDownLocks = 0 ;
4574+ numUpLocks = 0 ;
4575+ downLockRow = -1 ;
4576+ upLockRow = -1 ;
4577+
4578+ // consider objective function
4579+ if (model->col_cost_ [col] > 0 )
4580+ numUpLocks++;
4581+ else if (model->col_cost_ [col] < 0 )
4582+ numDownLocks++;
4583+
4584+ // check coefficients
4585+ for (const auto & nz : getColumnVector (col)) {
4586+ // update number of locks
4587+ if (hasImpliedBound (nz.index (), HighsInt{1 }, nz.value ())) {
4588+ // implied lower bound -> downlock
4589+ numDownLocks++;
4590+ downLockRow = nz.index ();
4591+ }
4592+ if (hasImpliedBound (nz.index (), HighsInt{-1 }, nz.value ())) {
4593+ // implied upper bound -> uplock
4594+ numUpLocks++;
4595+ upLockRow = nz.index ();
4596+ }
4597+
4598+ // stop early if there are locks in both directions, since the variable
4599+ // cannot be fixed in this case.
4600+ if (numDownLocks > 1 && numUpLocks > 1 ) break ;
4601+ }
4602+ };
4603+
4604+ // lambda for variable substitution
4605+ auto substituteCol = [&](HighsInt col, HighsInt row, HighsInt direction,
4606+ double colBound, double otherColBound) {
4607+ // check lhs and rhs for finiteness
4608+ bool lhsFinite = model->row_lower_ [row] != -kHighsInf ;
4609+ bool rhsFinite = model->row_upper_ [row] != kHighsInf ;
4610+
4611+ // use storeRow and getStoredRow since getRowVector's rowroot[row] would be
4612+ // overwritten by subsequent findNonZero calls, which would produce
4613+ // undefined behavior
4614+ storeRow (row);
4615+ for (const auto & rowNz : getStoredRow ()) {
4616+ // skip column index that was passed to this lambda
4617+ if (rowNz.index () == col) continue ;
4618+
4619+ // only consider non-fixed binary variables
4620+ if (model->integrality_ [rowNz.index ()] != HighsVarType::kInteger ||
4621+ model->col_lower_ [rowNz.index ()] != 0.0 ||
4622+ model->col_upper_ [rowNz.index ()] != 1.0 )
4623+ continue ;
4624+
4625+ // skip binary variable if setting it to its lower bound does not make the
4626+ // row redundant
4627+ if ((rhsFinite && impliedRowBounds.getResidualSumUpperOrig (
4628+ row, rowNz.index (), rowNz.value ()) >
4629+ model->row_upper_ [row] + primal_feastol) ||
4630+ (lhsFinite && impliedRowBounds.getResidualSumLowerOrig (
4631+ row, rowNz.index (), rowNz.value ()) <
4632+ model->row_lower_ [row] - primal_feastol))
4633+ continue ;
4634+
4635+ // now compute the implied lower bound (direction = 1) or implied upper
4636+ // bound (direction = -1) provided that the binary variable is set to its
4637+ // upper bound. store triplets (row, nonzero, nonzero) in a vector to
4638+ // speed up search
4639+ nzs.clear ();
4640+ if (colsize[col] < colsize[rowNz.index ()]) {
4641+ for (const auto & colNz : getColumnVector (col)) {
4642+ // skip non-zeros that do not yield an implied bound
4643+ if (!hasImpliedBound (colNz.index (), direction, colNz.value ()))
4644+ continue ;
4645+ HighsInt nzPos = findNonzero (colNz.index (), rowNz.index ());
4646+ if (nzPos == -1 ) continue ;
4647+ nzs.push_back ({colNz.index (), colNz.value (), Avalue[nzPos]});
4648+ }
4649+ } else {
4650+ for (const auto & colNz : getColumnVector (rowNz.index ())) {
4651+ HighsInt nzPos = findNonzero (colNz.index (), col);
4652+ if (nzPos == -1 ) continue ;
4653+ // skip non-zeros that do not yield an implied bound
4654+ if (!hasImpliedBound (colNz.index (), direction, Avalue[nzPos]))
4655+ continue ;
4656+ nzs.push_back ({colNz.index (), Avalue[nzPos], colNz.value ()});
4657+ }
4658+ }
4659+
4660+ // find best bound
4661+ double bestBound = -kHighsInf ;
4662+ for (const auto & triplet : nzs) {
4663+ // compute implied bound from row given that the binary variable is at
4664+ // its upper bound
4665+ double rhs = 0.0 ;
4666+ double residual = 0.0 ;
4667+ if (direction * triplet.jval < 0 ) {
4668+ rhs = model->row_upper_ [triplet.row ];
4669+ residual = impliedRowBounds.getResidualSumLowerOrig (triplet.row , col,
4670+ triplet.jval ) +
4671+ std::max (triplet.kval , 0.0 );
4672+ } else {
4673+ rhs = model->row_lower_ [triplet.row ];
4674+ residual = impliedRowBounds.getResidualSumUpperOrig (triplet.row , col,
4675+ triplet.jval ) +
4676+ std::min (triplet.kval , 0.0 );
4677+ }
4678+ // direction = 1: compute implied lower bound
4679+ // direction = -1: compute implied upper bound
4680+ double candidateBound = direction * (rhs - residual) / triplet.jval ;
4681+ // remember best bound (note the sign switch for direction < 0 above)
4682+ bestBound = std::max (bestBound, candidateBound);
4683+ }
4684+
4685+ // round bound
4686+ if (model->integrality_ [col] != HighsVarType::kContinuous )
4687+ bestBound = std::ceil (bestBound - primal_feastol);
4688+
4689+ // check if lower / upper bound is implied
4690+ if (bestBound >= direction * colBound - primal_feastol) {
4691+ // substitute variable
4692+ double offset = otherColBound;
4693+ double scale = colBound - otherColBound;
4694+ postsolve_stack.doubletonEquation (
4695+ -1 , col, rowNz.index (), 1.0 , -scale, offset, model->col_lower_ [col],
4696+ model->col_upper_ [col], 0.0 , false , false ,
4697+ HighsPostsolveStack::RowType::kEq , HighsEmptySlice ());
4698+ markColDeleted (col);
4699+ substitute (col, rowNz.index (), offset, scale);
4700+ HPRESOLVE_CHECKED_CALL (checkLimits (postsolve_stack));
4701+ break ;
4702+ }
4703+ }
4704+ return Result::kOk ;
4705+ };
4706+
4707+ // lambda for computing tighter bounds
4708+ auto hasTighterBound = [&](HighsInt col, HighsInt direction,
4709+ double currentBound, double & newBound) {
4710+ // return if objective coefficient has wrong sign
4711+ if (direction * model->col_cost_ [col] < 0 ) return false ;
4712+
4713+ // do not accept huge bounds
4714+ double hugeBound = primal_feastol / kHighsTiny ;
4715+
4716+ // initialise
4717+ newBound = -kHighsInf ;
4718+ currentBound *= direction;
4719+
4720+ for (const auto & nz : getColumnVector (col)) {
4721+ // get row index and coefficient
4722+ HighsInt row = nz.index ();
4723+ double val = nz.value ();
4724+
4725+ // skip rows that are already redundant
4726+ if (isRedundant (row)) continue ;
4727+
4728+ // initialise
4729+ double rhs = 0.0 ;
4730+ double residual = 0.0 ;
4731+
4732+ if (direction * val < 0.0 ) {
4733+ // skip rows with infinite rhs
4734+ rhs = model->row_upper_ [row];
4735+ if (rhs == kHighsInf ) continue ;
4736+
4737+ // compute residual
4738+ residual = impliedRowBounds.getResidualSumUpperOrig (row, col, val);
4739+ if (residual == kHighsInf ) return false ;
4740+ } else {
4741+ // skip rows with infinite lhs
4742+ rhs = model->row_lower_ [row];
4743+ if (rhs == -kHighsInf ) continue ;
4744+
4745+ // compute residual
4746+ residual = impliedRowBounds.getResidualSumLowerOrig (row, col, val);
4747+ if (residual == -kHighsInf ) return false ;
4748+ }
4749+
4750+ // compute bound
4751+ double candidateBound =
4752+ direction *
4753+ static_cast <double >((static_cast <HighsCDouble>(rhs) -
4754+ static_cast <HighsCDouble>(residual)) /
4755+ val);
4756+
4757+ // round up to make sure that all rows are redundant
4758+ candidateBound = std::ceil (candidateBound - primal_feastol);
4759+
4760+ // take largest bound
4761+ newBound = std::max (newBound, candidateBound);
4762+
4763+ // stop looking if bound is too large
4764+ if (newBound >= currentBound - primal_feastol || newBound > hugeBound)
4765+ return false ;
4766+ }
4767+
4768+ // return if no bound was found
4769+ if (newBound == -kHighsInf ) return false ;
4770+
4771+ // flip sign
4772+ newBound *= direction;
4773+ return true ;
4774+ };
4775+
4776+ // compute locks
4777+ HighsInt numDownLocks = 0 ;
4778+ HighsInt numUpLocks = 0 ;
4779+ HighsInt downLockRow = -1 ;
4780+ HighsInt upLockRow = -1 ;
4781+ computeLocks (col, numDownLocks, numUpLocks, downLockRow, upLockRow);
4782+
4783+ // check if variable can be fixed
4784+ if (numDownLocks == 0 || numUpLocks == 0 ) {
4785+ // fix variable
4786+ if (numDownLocks == 0 ? fixColToLowerOrUnbounded (postsolve_stack, col)
4787+ : fixColToUpperOrUnbounded (postsolve_stack, col)) {
4788+ // handle unboundedness
4789+ presolve_status_ = HighsPresolveStatus::kUnboundedOrInfeasible ;
4790+ return Result::kDualInfeasible ;
4791+ }
4792+ } else {
4793+ if (mipsolver != nullptr && model->col_lower_ [col] != -kHighsInf &&
4794+ model->col_upper_ [col] != kHighsInf ) {
4795+ // try substitution
4796+ if (numDownLocks == 1 && downLockRow != -1 ) {
4797+ HPRESOLVE_CHECKED_CALL (substituteCol (col, downLockRow, HighsInt{1 },
4798+ model->col_upper_ [col],
4799+ model->col_lower_ [col]));
4800+ } else if (numUpLocks == 1 && upLockRow != -1 ) {
4801+ HPRESOLVE_CHECKED_CALL (substituteCol (col, upLockRow, HighsInt{-1 },
4802+ model->col_lower_ [col],
4803+ model->col_upper_ [col]));
4804+ }
4805+ if (colDeleted[col]) return Result::kOk ;
4806+ }
4807+ // try to strengthen bounds
4808+ double newBound = 0.0 ;
4809+ if (hasTighterBound (col, HighsInt{1 }, model->col_upper_ [col], newBound)) {
4810+ // do not make bounds inconsistent
4811+ newBound = std::max (newBound, model->col_lower_ [col]);
4812+ // update upper bound
4813+ // only modify bounds on continuous variables if it leads to fixing
4814+ if (model->integrality_ [col] != HighsVarType::kContinuous ||
4815+ newBound == model->col_lower_ [col])
4816+ changeColUpper (col, newBound);
4817+ } else if (hasTighterBound (col, HighsInt{-1 }, model->col_lower_ [col],
4818+ newBound)) {
4819+ // do not make bounds inconsistent
4820+ newBound = std::min (newBound, model->col_upper_ [col]);
4821+ // update lower bound
4822+ // only modify bounds on continuous variables if it leads to fixing
4823+ if (model->integrality_ [col] != HighsVarType::kContinuous ||
4824+ newBound == model->col_upper_ [col])
4825+ changeColLower (col, newBound);
4826+ }
4827+ }
4828+ return Result::kOk ;
4829+ }
4830+
45264831HPresolve::Result HPresolve::initialRowAndColPresolve (
45274832 HighsPostsolveStack& postsolve_stack) {
45284833 // do a full scan over the rows as the singleton arrays and the changed row
0 commit comments