Алгоритм округления и закрепления числа до определенных концов

Я ищу эффективный алгоритм, который фиксирует целочисленное значение в диапазоне, а также меняет его конец по основанию 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, но также приветствуется любой псевдокод или другой императивный язык.

Я ищу эффективный алгоритм [...] Что вы пробовали? С какой проблемой вы столкнулись?

jub0bs 22.03.2024 21:12

Создать простое решение, которое зацикливается, фиксируя значение в диапазоне, затем добавляя/удаляя 1 каждый раз, а затем проверяя, соответствует ли оно одному из значений округления, и возвращая, если соответствует, тривиально. Однако это очень неэффективно для больших чисел.

Raven 22.03.2024 21:15

Я бы не назвал эти значения округления. Именно так строка должна заканчиваться по основанию 10. Если бы они округляли значения, я бы ожидал, что результат будет кратен этому числу.

maraca 22.03.2024 21:34

Спасибо за исправление, я назвал это так, потому что думал, что это легко вникнуть, но я обновил вопрос, чтобы использовать более правильную терминологию.

Raven 22.03.2024 21:38

Если вы передадите концы как числа, вы не сможете обеспечить, чтобы число заканчивалось на «00» (или более 0), только на «0», или вам нужно будет указать цифру перед ним, но только 0 невозможно. Я не знаю, задумано ли это.

maraca 22.03.2024 21:53

Могут быть методы получше, но если у вас не слишком много чисел в ends, вы можете попробовать вычислить наименьший префикс, который будет соответствовать каждому end в ends, который находится между желаемым диапазоном, а затем взять наименьшее общее число.

wLui155 22.03.2024 22:28

Бинарный поиск - это так.

Volker 22.03.2024 22:52

Разве не должно быть 7400 -> 7326?

Nick 23.03.2024 05:19
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
8
70
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Я думаю, вы знаете, как зажимать:

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
}

Примеры:

  • 2000: m = зажим(2000, 5350, 7392) = 5350, p = nextPowerOf10(26) = 100, m = m - m % p + end = 5350 - 5350 % 100 + 26 = 5326, кандидаты = [m - p , m, m + p] = [5226, 5326, 5426], результат = 5426 (только один в диапазоне).
  • 6205: кандидаты = [5201, 6201, 7201] => 6201
  • 7400: кандидаты = [6330, 7330, 8330] => 7330

Предполагая, что ваш список 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 раз быстрее. Смотрите мое редактирование

Nick 24.03.2024 01:40

Другие вопросы по теме