Я ищу эффективный алгоритм, который фиксирует целочисленное значение в диапазоне, а также меняет его конец по основанию 10 на один из нескольких концов переменной длины.
Это должно зажать value
между min
и max
, при этом конец изменится на один из конкретных концов, определенных в ends
, как можно ближе к исходному значению.
Пример входных данных:
ends: [26, 201, 330] (sorted list of integers, at least a factor 10 smaller than <value>)
min: 5350
max: 7392
value: 2000
-> 5426
value: 6205
-> 6201
value: 7400
-> 7330
Я реализую это на Go, но также приветствуется любой псевдокод или другой императивный язык.
Создать простое решение, которое зацикливается, фиксируя значение в диапазоне, затем добавляя/удаляя 1 каждый раз, а затем проверяя, соответствует ли оно одному из значений округления, и возвращая, если соответствует, тривиально. Однако это очень неэффективно для больших чисел.
Я бы не назвал эти значения округления. Именно так строка должна заканчиваться по основанию 10. Если бы они округляли значения, я бы ожидал, что результат будет кратен этому числу.
Спасибо за исправление, я назвал это так, потому что думал, что это легко вникнуть, но я обновил вопрос, чтобы использовать более правильную терминологию.
Если вы передадите концы как числа, вы не сможете обеспечить, чтобы число заканчивалось на «00» (или более 0), только на «0», или вам нужно будет указать цифру перед ним, но только 0 невозможно. Я не знаю, задумано ли это.
Могут быть методы получше, но если у вас не слишком много чисел в ends
, вы можете попробовать вычислить наименьший префикс, который будет соответствовать каждому end
в ends
, который находится между желаемым диапазоном, а затем взять наименьшее общее число.
Бинарный поиск - это так.
Разве не должно быть 7400
-> 7326
?
Я думаю, вы знаете, как зажимать:
clamp(n, min, max) = max(min, min(max, n))
Затем, чтобы удалить конец и заменить его тем, чем он должен заканчиваться, вам понадобится следующая более высокая степень 10:
nextPowerOf10(n) {
if (n == 0)
return 10 // mathematically not correct but works here
x = 1
while (n > 0) {
x = x * 10
n = n / 10 // integer division
}
return x
}
Таким образом, замена конца числа становится простой:
replace(n, end) = n - n % nextPowerOf10(end) + end
Затем вы можете проверить других кандидатов с обеих сторон, поэтому, когда m = replace(clamp(n, min, max), end)
и p = nextPowerOf10(end)
, другие кандидаты: m - p
и m + p
, все вместе:
clampEnd(n, min, max, end) {
m = clamp(n, min, max)
p = nextPowerOf10(end)
m = m - m % p + end
diff = -1
ret = -1
for (x in [m - p, m, m + p]) {
// this rounds down, make abs(n - x) <= diff to round up on equal distance
if (x >= min && x <= max && (diff < 0 || abs(n - x) < diff)) {
diff = abs(n - x)
ret = x
}
}
return ret // returns -1 if no number in range ends with end
}
Примеры:
Предполагая, что ваш список ends
не является огромным, вероятно, проще всего предварительно вычислить все допустимые значения между vmin
и vmax
, а затем выполнить двоичный поиск в этом списке, чтобы найти ближайшее значение. Вы можете реализовать это в Python следующим образом:
import math
def expand_ends(ends, vmin, vmax):
all_ends = []
for end in ends:
inc = 10 if end < 10 else 10 ** (int(math.log10(end)) + 1)
v = vmin - vmin % inc + end
if v < vmin:
v += inc
while True:
if v > vmax:
break
all_ends.append(v)
v += inc
return sorted(all_ends)
def search(values, v):
# check for end cases (clamp to range)
# this simplifies the search as we don't need to worry about not
# finding an insertion point in the list
if v <= values[0]:
return values[0]
if v >= values[-1]:
return values[-1]
# binary search for closest value
low = 0
hi = len(values)
while (low < hi):
mid = (low + hi) // 2
vmid = values[mid]
if vmid < v:
vmidp1 = values[mid+1]
if vmidp1 > v:
return vmid if (v - vmid) < (vmidp1 - v) else vmidp1
low = mid + 1
elif vmid > v:
vmidm1 = values[mid-1]
if vmidm1 < v:
return vmid if (vmid - v) < (v - vmidm1) else vmidm1
hi = mid
else:
return vmid
Использование:
ends = [26, 201, 330]
vmin, vmax = 5350, 7392
all_ends = expand_ends(ends, vmin, vmax)
# [5426, 5526, 5626, 5726, 5826, 5926, 6026, 6126, 6201, 6226, 6326, 6330, 6426, 6526, 6626, 6726, 6826, 6926, 7026, 7126, 7201, 7226, 7326, 7330]
search(all_ends, 2000)
# 5426
search(all_ends, 6205)
# 6201
search(all_ends, 6213)
# 6201
search(all_ends, 6214)
# 6226
search(all_ends, 6328)
# 6326
search(all_ends, 6329)
# 6330
search(all_ends, 7400)
# 7330
Обновлять
Я преобразовал код другого ответа в Python и использовал timeit
для сравнения двух решений, перебирающих каждый ввод от vmin-100
до vmin+100
:
timeit.timeit(setup='''
import math
def expand_ends(ends, vmin, vmax):
all_ends = []
for end in ends:
inc = 10 if end < 10 else 10 ** (int(math.log10(end)) + 1)
v = vmin - vmin % inc + end
if v < vmin:
v += inc
while True:
if v > vmax:
break
all_ends.append(v)
v += inc
return sorted(all_ends)
def search(values, v):
# check for end cases (clamp to range)
# this simplifies the search as we don't need to worry about not
# finding an insertion point in the list
if v <= values[0]:
return values[0]
if v >= values[-1]:
return values[-1]
# binary search for closest value
low = 0
hi = len(values)
while (low < hi):
mid = (low + hi) // 2
vmid = values[mid]
if vmid < v:
vmidp1 = values[mid+1]
if vmidp1 > v:
return vmid if (v - vmid) < (vmidp1 - v) else vmidp1
low = mid + 1
elif vmid > v:
vmidm1 = values[mid-1]
if vmidm1 < v:
return vmid if (vmid - v) < (v - vmidm1) else vmidm1
hi = mid
else:
return vmid
ends = [26, 201, 330]
vmin, vmax = 5350, 7392
''', stmt='''
all_ends = expand_ends(ends, vmin, vmax)
out = [search(all_ends, v) for v in range(vmin-100, vmax+100)]
''', number = 1_000)
timeit.timeit(setup='''
def nextPowerOf10(n):
if n == 0:
return 10 # mathematically not correct but works here
x = 1
while n > 0:
x = x * 10
n = n // 10 # integer division
return x
def clampEnd(n, vmin, vmax, end):
m = max(vmin, min(vmax, n))
p = nextPowerOf10(end)
m = m - m % p + end
diff = -1
ret = -1
for x in [m - p, m, m + p]:
# this rounds down, make abs(n - x) <= diff to round up on equal distance
if vmin <= x <= vmax and (diff < 0 or abs(n - x) < diff):
diff = abs(n - x)
ret = x
return ret # returns -1 if no number in range ends with end
def bestEnd(v, ends):
diffs = [abs(v-end) for end in ends]
return ends[diffs.index(min(diffs))]
ends = [26, 201, 330]
vmin, vmax = 5350, 7392
''', stmt='''
out = [bestEnd(v, [clampEnd(v, vmin, vmax, end) for end in ends]) for v in range(vmin-100, vmax+100)]
''', number = 1_000)
На моем компьютере мое решение занимает 0,57 секунды, а другое решение — 4,56 секунды.
@Raven не практично вычислять список всех возможных целей? Если да, то двоичный поиск в этом списке выполняется намного быстрее, чем итерация процедуры ограничения. При преобразовании вашего кода в Python мой код работает почти в 10 раз быстрее. Смотрите мое редактирование
Я ищу эффективный алгоритм [...] Что вы пробовали? С какой проблемой вы столкнулись?