Алгоритм C++ для разделения двух целочисленных интервалов со знаком
Рассмотрим интервалы A = [x1, y1] и B = [x2, y2], два интервала, представляющие целые числа со знаком.
Интервальная арифметическая страница в Википедии дает формулу для решения случая, когда B не содержит 0, реализация C++ может быть следующей:
void print(const char* const what, int x, int y)
{
std::cout << what << " = [" << x << ", " << y << "]\n";
}
void div(const char* const what, int x1, int y1, int x2, int y2)
{
int v1 = x1/x2;
int v2 = x1/y2;
int v3 = y1/x2;
int v4 = y1/y2;
int x = std::min(std::min(v1, v2), std::min(v3, v4));
int y = std::max(std::max(v1, v2), std::max(v3, v4));
print(what, x, y);
}
int main()
{
int x1 = -10000, y1 = 150000;
int x2 = -10, y2 = 10;
print("A", x1, y1);
print("B", x2, y2);
div("A/B", x1, y1, x2, y2);
}
Выход:
A = [-10000, 150000]
B = [-10, 10]
A/B = [-15000, 15000]
Как и ожидалось, результат неверен, учитывая, что B содержит 0. Например, поскольку 1 является частью B, 150000 должно быть в пределах A/B, но это не так.
Что такое жизнеспособный алгоритм, когда 0 исключен из B? Должно ли решение использовать несколько интервалов около -1 и 1 (т.е. исключать 0) и как-то их объединить?
Изменить: решение может быть выражено через объединение (вектор) интервалов типа long long.
1 ответ
Вы не пишете C++, вы пишете C, завернутый в какой-то маленький C++. Вот что я хотел бы сделать:
Сначала я бы начал с класса для интервала, то есть:
struct Interval
{
int left;
int right;
};
Тогда я бы начал с сложения, вычитания и умножения. Например
auto operator+(Interval lhs, Interval rhs) -> Interval;
Которые довольно просты.
Когда мы добираемся до деления, как видно из вашей вики-ссылки, все становится сложнее.
- Результатом является уже не интервал, а мультиинтервал, то есть соединение интервалов.
- Эти интервалы имеют
∞
а также-∞
головы.
Первая проблема решается с новым классом:
class MultiInterval
{
std::vector<Interval> intervals_;
//...
};
Для второй проблемы... ну мы больше не можем использовать int
как данные. Поэтому вам нужен новый класс для целых чисел, который может содержать значения ∞, -∞, NaN
, NaN
требуется как побочный продукт, например -∞ + ∞ = NaN
,
class ExtendedInt
{
// black magic
};
для которого вы должны будете определить 3 константы, которые являются новыми 3 значениями. Затем вам нужно будет определить все основные арифметические операции над ними.
Наконец, мы можем переделать все в нечто вроде этого:
class ExtendedInt;
auto operator+(ExtendedInt lhs, ExtendedInt rhs) -> ExtendedInt;
auto operator-(ExtendedInt lhs, ExtendedInt rhs) -> ExtendedInt;
auto operator*(ExtendedInt lhs, ExtendedInt rhs) -> ExtendedInt;
auto operator/(ExtendedInt lhs, ExtendedInt rhs) -> ExtendedInt;
// etc...
struct Interval
{
ExtendedInt left;
ExtendedInt right;
};
class MultiInterval
{
std::vector<Interval> intervals_;
//...
};
auto operator+(Interval lhs, Interval rhs) -> Interval;
auto operator-(Interval lhs, Interval rhs) -> Interval;
auto operator*(Interval lhs, Interval rhs) -> Interval;
auto operator/(Interval lhs, Interval rhs) -> MultiInterval;
Как видите, все довольно быстро усложняется.
Что касается реализации ExtendedInt, одно решение состоит в том, чтобы иметь два элемента данных, один int
и одно перечисление
enum class ExtendedIntValues { Normal, Inf, NegInf, NaN };
class ExtendedInt
{
int value_;
ExtendedIntValues ext_value_;
};
если ext_value_ == Normal
тогда значение экземпляра value_
иначе это ext_value_
,
Другое решение состоит в том, чтобы понять, что функционально это союз. Так что вы могли бы использовать std::variant<int, ExtendedIntValues>
вместо.
Еще одно решение заключается в использовании std::optional
:
enum class ExtendedIntValues { Inf, NegInf, NaN }; // notice no Normal enum
class ExtendedInt
{
std::optional<int> value_;
ExtendedIntValues ext_value_;
};
Все эти решения жертвуют пространством с std::variant
жертвуя юзабилити.
Другое решение, которое просто жертвует 3 нормальными int
значения должны иметь только один int
и имеют некоторые значения, представляющие особые случаи:
class ExtendedInt
{
int value_;
};
constexpr ExtededInt NegInf = ...;
constexpr ExtededInt Inf = ...;
constexpr ExtededInt NaN = ... ;
Внутренне:
std::numeric_limits<int>::min()
неоспоримымNegInf
std::numeric_limits<int>::max() - 1
являетсяInf
std::numeric_limits<int>::max()
являетсяNaN
Или что-то в этом роде, но это должно быть сделано совершенно прозрачным. Особое внимание должно быть уделено арифметическим операциям.
Другое решение состоит в том, чтобы понять, что уже есть тип (или два), которые изначально поддерживают эти значения и идут с float
или же double
, Это принесет в жертву точность.
Когда у вас есть ExtendedInt
разобрался потом просто следуй вики алгоритму:
auto operator/(Interval lhs, Interval rhs) -> MultiInterval
{
MultiInterval f1{lhs};
MultiInterval f2{};
if (!contains(rhs, 0))
f2 = MultiInterval{{1 / rhs.right, 1 / rhs.left}};
else if (rhs.right == 0)
f2 = MultiInterval{{NegInf, 1 / rhs.left}};
else if (rhs.left == 0)
f2 = MultiInterval{{1 / rhs.right, Inf}};
else
f2 = MultiInterval{{NegInf, 1 / rhs.left}, {1 / rhs.right, Inf}};
return f1 * f2;
}
Минимальный интерфейс для работы выше:
struct ExtendedInt
{
ExtendedInt();
ExtendedInt(int);
};
ExtendedInt NegInf;
ExtendedInt Inf;
ExtendedInt NaN;
auto operator+(ExtendedInt, ExtendedInt) -> ExtendedInt;
auto operator-(ExtendedInt, ExtendedInt) -> ExtendedInt;
auto operator*(ExtendedInt, ExtendedInt) -> ExtendedInt;
auto operator/(ExtendedInt, ExtendedInt) -> ExtendedInt;
auto operator==(ExtendedInt, ExtendedInt) -> bool;
auto operator!=(ExtendedInt, ExtendedInt) -> bool;
struct Interval
{
ExtendedInt left, right;
};
auto contains(Interval, ExtendedInt) -> bool;
class MultiInterval
{
public:
MultiInterval(std::initializer_list<Interval>);
};
auto operator*(MultiInterval lhs, MultiInterval rhs) -> MultiInterval;
Наконец, обратите внимание на это из Википедии:
Поскольку в арифметическом вычислении интервалов может происходить несколько таких делений, иногда полезно выполнять вычисления с так называемыми множественными интервалами вида
Таким образом, вам в конечном итоге придется работать только с MultiInterval
где интервал является MultiInterval
только с одним интервалом.