整数変数と連立方程式の求解

QUBO++ では,バイナリ変数に加えて整数変数を使えます. 整数変数は値の範囲(下限と上限)を指定して宣言し, QUBO++ にバンドルされているソルバー (Easy Solver・Exhaustive Solver・ABS3 Solver)が 整数値のまま直接探索します.

整数変数の宣言

qbpp::int_var("name") を範囲演算子 <= <= ではさむと, その範囲の整数値をとる変数が作られます.範囲の指定は必須です. 宣言した変数は,バイナリ変数と同じように式の中で使えます:

#include <qbpp/qbpp.hpp>
#include <qbpp/exhaustive_solver.hpp>

int main() {
  auto x = 0 <= qbpp::int_var("x") <= 10;
  auto f = x * x - 4 * x;
  f.simplify();
  std::cout << "f = " << f << std::endl;

  auto solver = qbpp::ExhaustiveSolver(f);
  auto sol = solver.search();
  std::cout << "x = " << sol(x) << std::endl;
  std::cout << "f = " << sol.energy() << std::endl;
}

x は 0 以上 10 以下の整数値をとる変数で,Exhaustive Solver が $f = x^2 - 4x$ を最小にする x を求めます. プログラムの出力は以下の通りです:

f = -4*x +x*x
x = 2
f = -4

x * x には,バイナリ変数の規則($x \cdot x = x$)は適用されず, 整数の 2 乗としてそのまま扱われる点に注意してください. このように整数変数だけの式は simplify() で簡約します. バイナリ変数を含む式は,バイナリ変数の規則も使う simplify_as_binary() で簡約します. 整数変数だけの式にも simplify_as_binary() は使えますが,存在しないバイナリ変数の規則を適用しようとするので,効率が悪くなる可能性があります.

連立方程式を解く

整数変数を使って,次の連立方程式を解いてみます(解は $x=6$,$y=4$ です):

\[\begin{aligned} x + y &= 10\\ 2x+4y &= 28 \end{aligned}\]

それぞれの方程式の左辺と右辺の差を 2 乗して足した式

\[\begin{aligned} f(x,y) &= (x+y-10)^2 + (2x+4y-28)^2 \end{aligned}\]

は,2 つの方程式が同時に満たされるとき,かつそのときに限り最小値 0 をとります. したがって,$f(x,y)$ を最小にする $x$ と $y$ が連立方程式の解です. 次のプログラムは,範囲 $[0,10]$ の整数変数 x,y で $f(x,y)$ を作り,Easy Solver で解きます:

#include <qbpp/qbpp.hpp>
#include <qbpp/easy_solver.hpp>

int main() {
  auto x = 0 <= qbpp::int_var("x") <= 10;
  auto y = 0 <= qbpp::int_var("y") <= 10;
  auto f = qbpp::sqr(x + y - 10) + qbpp::sqr(2 * x + 4 * y - 28);
  f.simplify();
  std::cout << "f = " << f << std::endl;

  auto solver = qbpp::EasySolver(f);
  auto sol = solver.search({{"target_energy", 0}});
  std::cout << "x = " << sol(x) << ", y = " << sol(y) << std::endl;
  std::cout << "f = " << sol.energy() << std::endl;
}

最小値が 0 であることが分かっているので,target_energy に 0 を指定し, エネルギー 0 の解が見つかった時点で探索を終了させています. プログラムの出力は以下の通りです:

f = 884 -132*x -244*y +5*x*x +18*x*y +17*y*y
x = 6, y = 4
f = 0

f は x と y の 2 次式のまま保持され,ソルバーは x と y の値を整数のまま探索します.

整数変数の配列

int_var() に次元を渡すと,整数変数の配列が作れます. 要素には s[i] でアクセスし,qbpp::sum() で総和の式が作れます:

#include <qbpp/exhaustive_solver.hpp>
#include <qbpp/qbpp.hpp>

int main() {
  auto s = 0 <= qbpp::int_var("s", 3) <= 9;
  auto t = qbpp::sum(s);
  auto f = qbpp::sqr(t - 6) + qbpp::sqr(s[0] - s[1] + 1) +
           qbpp::sqr(s[1] - s[2] + 1);
  f.simplify();
  auto solver = qbpp::ExhaustiveSolver(f);
  auto sol = solver.search();
  std::cout << "s = " << sol(s[0]) << ", " << sol(s[1]) << ", " << sol(s[2])
            << std::endl;
  std::cout << "f = " << sol.energy() << std::endl;
}

プログラムの出力は以下の通りです:

s = 1, 2, 3
f = 0

配列全体の演算(s + t,2 * s,qbpp::sqr(s),s <= 5 のような 要素ごとの制約,s(qbpp::slice(1, 3)),qbpp::vector_sum(), qbpp::concat())は Expr の配列として扱われ, sol(s) で全要素の値をまとめて取り出せます. qbpp::einsum() に渡すときだけ,qbpp::toExpr(s) で Expr 配列に 変換してください.

要素ごとに範囲が異なる配列

配列形の宣言は,全要素が同じ範囲を共有します.範囲の下限・上限に 配列を指定すると,要素ごとに個別の範囲を持つ整数変数の 配列が作られます.下限と上限が等しい要素は,その値に固定された 定数になります:

#include <qbpp/exhaustive_solver.hpp>
#include <qbpp/qbpp.hpp>

int main() {
  const auto lower = qbpp::array({1, 0, 2});
  const auto upper = qbpp::array({4, 3, 2});
  auto v = lower <= qbpp::int_var("v", 3) <= upper;
  auto f = qbpp::sqr(qbpp::sum(v) - 6) + v[0];
  f.simplify();
  auto solver = qbpp::ExhaustiveSolver(f);
  auto sol = solver.search();
  std::cout << "v = " << sol(v[0]) << ", " << sol(v[1]) << ", " << sol(v[2])
            << std::endl;
  std::cout << "f = " << sol.energy() << std::endl;
}

プログラムの出力は以下の通りです(v[2] は下限=上限なので 定数 2 に固定されます):

v = 1, 3, 2
f = 1

片側だけ配列にすることもできます(例: 0 <= qbpp::int_var("w", 3) <= upper は下限 0 を全要素で共有します).2 次元以上の配列(例: qbpp::int_var("x", 3, 4))でも 同じように書けます.下限・上限の配列は変数の配列と同じ形にし,形が違うと std::invalid_argument になります.

また,qbpp::int_var("x", 3) == 0 は全要素が定数 0 の可変配列を作り, 各要素に整数変数をひとつずつ代入できます:

#include <qbpp/exhaustive_solver.hpp>
#include <qbpp/qbpp.hpp>

int main() {
  const auto cap = qbpp::array({3, 1, 2});
  auto x = qbpp::int_var("x", 3) == 0;
  for (size_t j = 0; j < cap.size(); j++) {
    x[j] = 0 <= qbpp::int_var() <= cap[j];
  }
  auto f = qbpp::sqr(qbpp::sum(x) - 5) - x[0] - x[1];
  f.simplify();
  auto solver = qbpp::ExhaustiveSolver(f);
  auto sol = solver.search();
  std::cout << "x = " << sol(x[0]) << ", " << sol(x[1]) << ", " << sol(x[2])
            << std::endl;
  std::cout << "f = " << sol.energy() << std::endl;
}

プログラムの出力は以下の通りです:

x = 3, 1, 1
f = -4

代入しなかった要素は定数(ここでは 0)のまま式に残ります. 定数には 0 以外の値も指定できます.

値の評価と置換

宣言した整数変数に値を与えて式の値を求めるには,バイナリ変数と同じく 評価関数 f({{x, 3}}) を使います. 与える値は宣言した範囲内でなければなりません(範囲外の値の扱いは後述). 置換関数 qbpp::replace(f, {{x, e}}) は,式 f に現れる x をすべて式 e に書き換えます. x * x のような冪は出現ごとに置き換えられるので,x を y + 1 で置換すると $(y+1)^2 + (y+1)$ が展開されます:

#include <qbpp/qbpp.hpp>

int main() {
  auto x = 0 <= qbpp::int_var("x") <= 10;
  auto y = 0 <= qbpp::int_var("y") <= 10;
  auto f = x * x + x;

  std::cout << "f(3) = " << f({{x, 3}}) << std::endl;

  auto g = qbpp::replace(f, {{x, 2}});
  g.simplify();
  std::cout << "g = " << g << std::endl;

  auto h = qbpp::replace(f, {{x, y + 1}});
  h.simplify();
  std::cout << "h = " << h << std::endl;
}

プログラムの出力は以下の通りです:

f(3) = 12
g = 6
h = 2 +3*y +y*y

置換は記号的な書き換えで,x の範囲は結果に引き継がれません. y + 1 が x の範囲に収まるべきだという制約が自動的に付くことはなく,範囲外の定数で置換しても例外にはなりません(下限と上限が等しい固定の整数変数だけは,その値以外で置換すると std::invalid_argument になります). 必要な制約は qbpp::cons() で明示してください. 結果の式が整数変数を含むかどうかは,置換に使った式に含まれる変数で決まります (y + 1 なら y の範囲と整数性がそのまま生きます). 変数 x 自体の宣言は消えず,別の式で引き続き使えます. なお,評価関数に範囲外の値を与えた場合は例外(std::invalid_argument)になります.

制約

整数変数を含む式の制約($2x + 3y \leq 24$ など)は,qbpp::cons() で書けます. 範囲制約と整数線形計画法の求解で説明します.

外部ソルバーとの連携

MIP ソルバーは整数変数をそのまま扱えるため,整数変数は整数の列として直接渡されます. 目的関数が線形で制約を qbpp::cons() で書いたモデルは,qbpp::ScipSolver solver(f, qbpp::ilp); のように ILP オプションを付ければ MIP ソルバー(SCIP, HiGHS, CBC, GLPK) すべてで解けます.

一方,バイナリ変数しか扱えない外部の QUBO ソルバーに渡す場合は, 整数変数をバイナリ変数で表す必要があります. その方法はバイナリエンコードの整数変数で説明します.

整数変数と one-hot 表現の使い分け

  • 個数・時刻・在庫量など数量を表す変数には,整数変数が適しています.
  • 「どれを選ぶか」というラベル(訪問順序・色など)には, 整数変数よりも one-hot 表現 (置換行列・One-hot 制約)が適しています.

なお,ライセンスの変数数の上限では,整数変数 1 個は, その範囲(上限 − 下限)を $r$ とすると,次の個数のバイナリ変数分として カウントされます:

\[\lceil \log_2 (r+1) \rceil\]

詳細はライセンス管理を参照してください.


Back to top

Page last modified: 2026.10.30.

© 2026 中野浩嗣, 広島大学