@@ -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
46354850HPresolve::Result HPresolve::initialRowAndColPresolve (
0 commit comments