python

Найти вероятность выпадения k (сумма выпавших значений) при бросании n кубиков (часть 2 из 2)

  • четверг, 1 сентября 2022 г. в 00:47:40
https://habr.com/ru/post/685552/
  • Занимательные задачки
  • Python
  • JavaScript
  • Алгоритмы
  • Математика


Введение

В своей предыдущей статье я описал способ нахождения делимого вероятности выпадения какой-то суммы чисел на кубиках при помощи многократной свёртки последовательности [1 1 1 1 1 1] на саму себя. Иными словами, многократное умножение в столбик (без переноса переполнившихся разрядов) последовательности/числа 111111 на саму/само себя. Почему, правда, не пишут, что умножение в столбик является прямой аналогией свёртки последовательностей — для меня загадка (может я что-то упускаю из вида - если я не прав, пожалуйста, напишите). Однако, дальше в статье я буду применять два словосочетания "свёртка последовательностей" и "умножение в столбик" совместно, т.к. первое — корректное описание операции, а второе отвечает за наглядность и простоту восприятия.

Напомню:

Так же в конце предыдущей статьи я "страшился" найти вероятности для 1000 кубиков. Вот именно этим и предлагаю заняться.

Прелюдия

Хотелось бы подчеркнуть, что и на картинке вверху, и собственно в предыдущей статье упор делался на "умножение в столбик" / свёртку последовательностей [1 1 1 1 1 1], и не затрагивалась возможность "умножить" на что-либо ещё. Вот эту оплошность хотелось бы упразднить.

Отвлекусь немного на факт, что любое натуральное число может быть представлено как сумма натуральных степеней числа 2 (Производящие функции — туда и обратно ответ на вопрос: какие грузы можно взвесить с помощью гирь в 20, 21, 22,..., 2n грамм и сколькими способам?). Приведу визуализацию:

Данное знание нам будет полезно для операций со степенями. Например, нахождение какого-то числа a63 будем представлять как: a63 = a1 + 2 + 4 + … + 32 = a1 * a2 * a4 * … * a32. То есть зная только I элемент и умея умножать/свёртывать будем пытаться найти 63-й элемент (степень 63) используя как можно меньше операций умножения/свёртки.

Для начала хотелось бы обкатать операцию свёртки последовательностей / "умножение в столбик" на уже знакомом треугольнике Паскаля. А именно попробовать найти 9-й элемент треугольника Паскаля зная только I элемент (именно [1 1]) не при помощи многократной свёртки последовательностей / "умножение в столбик" [1 1] на саму себя (дискретная свёртка или полиномиальное умножение), а представив что каждая последовательность чисел в треугольнике Паскаля соответствует степени последовательности [1 1]. Звучит наверно запутанно, так что приведу картинку из прошлой статьи, для визуализации:

Попробуем найти 9-й элемент треугольника Паскаля.

Выглядит многообещающе. Так же приложу скрипт с теми же результатами при помощи именно свёртки последовательностей.

Python. Пример. II, IV, VIII, XI элемент треугольника Паскаля
# -*- coding: utf-8 -*-

import numpy

convolve_out = numpy.convolve([1, 1], [1, 1])
# [1 2 1]
print(convolve_out)

convolve_out = numpy.convolve(convolve_out, convolve_out)
# [1 4 6 4 1]
print(convolve_out)

convolve_out = numpy.convolve(convolve_out, convolve_out)
# [ 1 8 28 56 70 56 28 8 1]
print(convolve_out)

convolve_out = numpy.convolve(convolve_out, [1, 1])
# [ 1 9 36 84 126 126 84 36 9 1]
print(convolve_out)

Визуализация алгоритма, I попытка

По аналогии с треугольником Паскаля хочется провернуть аналогичную операцию с кубиками, и найти для примера вероятность выпадения суммы костей 19 для 5 кубиков. Т.е. возьмём первоначальную последовательность [1 1 1 1 1 1] и дойдём до 5-ого кубика (степень 5) по следующей цепочке операций свёртки последовательностей / "умножение в столбик":

a1 * a1 = a2

a2 * a2 = a4

a1 * a4 = a5

Приложу скрипт для нахождения “Сколько раз встречается значение” в “Этап I. Генерация 2-х списков/массивов: Значения (сумма выпавших костей) И Сколько раз встречается значение” при помощи свёртки последовательностей / “умножения в столбик”.

Python. Пример. Свёртка последовательностей [1 1 1 1 1 1]
# -*- coding: utf-8 -*-

import numpy

convolve_out = numpy.convolve([1, 1, 1, 1, 1, 1], [1, 1, 1, 1, 1, 1])
# [1 2 3 4 5 6 5 4 3 2 1]
print(convolve_out)

convolve_out = numpy.convolve(convolve_out, convolve_out)
# [ 1 4 10 20 35 56 80 104 125 140 146 140 125 104 80 56 35 20 10 4 1]
print(convolve_out)

convolve_out = numpy.convolve(convolve_out, [1, 1, 1, 1, 1, 1])
# [ 1 5 15 35 70 126 205 305 420 540 651 735 780 780 735 651 540 420 305 205 126 70 35 15 5 1]
print(convolve_out)

Скрипты, I попытка

В целом, как мне кажется, задумка достаточно расписана. Остаётся выложить получившейся скрипты, написанные по описанным лекалам. Простор для оптимизаций оставляю читателям.

Python
# -*- coding: utf-8 -*-

def main():
    c_int_side_dice: int = 6  # сколько граней у кубика
    c_int_dice_number: int = 1000  # кол-во кубиков
    c_int_number_to_find: int = 2000  # число, вероятность выпадения которого хотим найти
    probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice)
    print(probability)


# собственно поиск вероятности определённого значения
def dice_probability(int_dice_number: int, int_number_to_find: int, c_int_side_dice: int) -> float:
    if int_number_to_find >= int_dice_number and int_number_to_find <= c_int_side_dice * int_dice_number:
        list_values: list[int] = [i for i in range(int_dice_number, c_int_side_dice * int_dice_number + 1)]
        list_interm_probability = interm_probabilities(c_int_side_dice, int_dice_number)

        for i in range(len(list_values)):
            if list_values[i] == int_number_to_find:
                int_out: int = list_interm_probability[i]
                break
        return int_out / (c_int_side_dice ** int_dice_number)
    else:
        # задаваемое число выходит за рамки реально возможного диапазона значений
        return 0.0


# возвращает список/массив: сколько раз встречается значение
def interm_probabilities(int_side_dice: int, int_pow: int) -> list[int]:
    """
    На примере int_side_dice = 6, int_pow = 5
    {
      1: [1, 1, 1, 1, 1, 1],
      2: [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1],
      4: [1, 4, 10, 20, 35, 56, 80, 104, 125, 140, 146, 140, 125, 104, 80, 56, 35, 20, 10, 4, 1]
      5: [1, 5, 15, 35, 70, 126, 205, 305, 420, 540, 651, 735, 780, 780, 735, 651, 540, 420, 305, 205, 126, 70, 35, 15, 5, 1]
    }
    """
    dict_interm_probability: dict[int, list[int]] = {1: [1] * int_side_dice}
    if int_pow == 0:
        print("Не поддерживается")
        quit()
    elif int_pow != 1:
        list_to_do = map_todo(int_pow) 
        for elem in list_to_do:
            dict_interm_probability[elem[2]] = multiply_cins_orig(dict_interm_probability[elem[0]], dict_interm_probability[elem[1]])
    return dict_interm_probability[int_pow]


# Как добраться до интересующего значения, используя x2/+nx для степеней
def map_todo(int_wanted: int) -> list[tuple[int, int, int]]:
    """
    На примере int_wanted = 5
    Степени "числа":
    1
    1 * 2 = 2 -> tuple(1, 1, 2)
    2 * 2 = 4 -> tuple(2, 2, 4)
    4 + 1 = 5 -> tuple(4, 1, 5)
    """

    int_current_id: int = 1
    int_sum: int = 1
    b_ascending: bool = True
    list_solution: list[tuple[int, int, int]] = []

    while True:
        if int_sum == int_wanted:
            break
        elif b_ascending and 2 * int_current_id <= int_wanted:
            list_solution.append(  # mult_1, mult_2, result
                (int_current_id, int_current_id, 2 * int_current_id)
            )
            int_current_id = 2 * int_current_id
            int_sum = int_current_id
        elif b_ascending and 2 * int_current_id > int_wanted:
            b_ascending = False
            int_sum = int_current_id
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
        elif not b_ascending and int_sum + int_current_id <= int_wanted:
            list_solution.append(  # mult_1, mult_2, result
                (int_sum, int_current_id, int_sum + int_current_id)
            )
            int_sum = int_sum + int_current_id
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
        elif not b_ascending and int_sum + int_current_id > int_wanted:
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
    return list_solution


# "умножение" в столбик двух массивов/списков
def multiply_cins_orig(list_in_1: list[int], list_in_2: list[int]) -> list[int]:
    int_len_2: int = len(list_in_2)
    list_dummy: list[list[int]] = []
    for i in range(int_len_2):
        list_dummy.append([0] * i)  # [], [0], [0, 0], [0, 0, 0] ...

    list_for_sum: list[list[int]] = []
    i: int = -1
    for elem_2 in list_in_2:
        i += 1
        list_interm: list[int] = [elem_1 * elem_2 for elem_1 in list_in_1]
        list_for_sum.append(list_dummy[i] + list_interm + list_dummy[int_len_2 - i - 1])

    """
    [list_in_1 X elem_2[0], 0, 0, 0, 0, 0]
    [0, list_in_1 X elem_2[1], 0, 0, 0, 0]
    [0, 0, list_in_1 X elem_2[2], 0, 0, 0]
    [0, 0, 0, list_in_1 X elem_2[3], 0, 0]
    [0, 0, 0, 0, list_in_1 X elem_2[4], 0]
    [0, 0, 0, 0, 0, list_in_1 X elem_2[5]]
    """

    list_out: list[int] = []
    for i in range(len(list_for_sum[0])):
        sum_out: int = 0
        for j in range(int_len_2):
            sum_out += list_for_sum[j][i]
        list_out.append(sum_out)
    """
    [1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]
    """
    return list_out


main()

JavaScript
function main(){
    const c_int_side_dice = 6;  // сколько граней у кубика
    const c_int_dice_number = 100; // кол-во кубиков
    const c_int_number_to_find = 300; // число, вероятность выпадения которого хотим найти
    let probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice);
    console.log(probability);
}


// собственно поиск вероятности определённого значения
function dice_probability(int_dice_number, int_number_to_find, c_int_side_dice){
    if (int_number_to_find >= int_dice_number && int_number_to_find <= c_int_side_dice * int_dice_number){
        let list_values = new Array();
        let i = 0;
        for (let j = int_dice_number; j <= c_int_side_dice * int_dice_number; j++){
            list_values[i] = j;
            i++;
        }
        let list_interm_probability = interm_probabilities(c_int_side_dice, int_dice_number);
        let int_out;
        for (let i = 0; i <= list_values.length; i++){
            if (list_values[i] == int_number_to_find){
                int_out = list_interm_probability[i];
                break;
            }
        }
        return int_out / Math.pow(c_int_side_dice, int_dice_number);
    } else {
        // задаваемое число выходит за рамки реально возможного диапазона значений
        return 0.0;
    }
}

// возвращает список/массив: сколько раз встречается значение
function interm_probabilities(int_side_dice, int_pow){
    // На примере int_side_dice = 6, int_pow = 5
    // {
    //   1: [1, 1, 1, 1, 1, 1],
    //   2: [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1],
    //   4: [1, 4, 10, 20, 35, 56, 80, 104, 125, 140, 146, 140, 125, 104, 80, 56, 35, 20, 10, 4, 1]
    //   5: [1, 5, 15, 35, 70, 126, 205, 305, 420, 540, 651, 735, 780, 780, 735, 651, 540, 420, 305, 205, 126, 70, 35, 15, 5, 1]
    // }
    let dict_interm_probability = {1: Array(int_side_dice).fill(1)};
    if (int_pow == 0){
        console.log("Не поддерживается");
        return;
    } else if (int_pow != 1){
        let list_to_do = map_todo(int_pow); 
        for (let i = 0; i < list_to_do.length; i++){
            dict_interm_probability[list_to_do[i][2]] = multiply_cins_orig(dict_interm_probability[list_to_do[i][0]], dict_interm_probability[list_to_do[i][1]]);
        }
    }
    return dict_interm_probability[int_pow];
}


// Как добраться до интересующего значения, используя x2/+nx для степеней
function map_todo(int_wanted){
    // На примере int_wanted = 5
    // Степени "числа":
    // 1
    // 1 * 2 = 2 -> Array(1, 1, 2)
    // 2 * 2 = 4 -> Array(2, 2, 4)
    // 4 + 1 = 5 -> Array(4, 1, 5)

    let int_current_id = 1;
    let int_sum = 1;
    let b_ascending = true;
    let list_solution = new Array();
    let i = 0;
    while (true){
        if (int_sum == int_wanted){
            break;
        } else if (b_ascending && 2 * int_current_id <= int_wanted){
            list_solution[i] = [int_current_id, int_current_id, 2 * int_current_id];  // mult_1, mult_2, result
            i++;
            int_current_id = 2 * int_current_id;
            int_sum = int_current_id;
        } else if (b_ascending && 2 * int_current_id > int_wanted){
            b_ascending = false;
            int_sum = int_current_id;
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        } else if (!b_ascending && int_sum + int_current_id <= int_wanted){
            list_solution[i] = [int_sum, int_current_id, int_sum + int_current_id];  // mult_1, mult_2, result
            i++;
            int_sum = int_sum + int_current_id;
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        } else if (!b_ascending && int_sum + int_current_id > int_wanted){
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        }
    }
    return list_solution;
}

// "умножение" в столбик двух массивов/списков
function multiply_cins_orig(list_in_1, list_in_2){
    let int_len_1 = list_in_1.length;
    let int_len_2 = list_in_2.length;
    
    let list_dummy = new Array();
    for (let j = 0; j < int_len_2; j++){
        list_dummy[j] = Array(j).fill(0);  // [], [0], [0, 0], [0, 0, 0] ...
    }

    let list_for_sum = new Array();
    for (let j = 0; j < int_len_2; j++){
        let list_interm = new Array();
        for (let i = 0; i < int_len_1; i++){
            list_interm[i] = list_in_1[i] * list_in_2[j]
        }
        list_for_sum[j] = list_dummy[j].concat(list_interm, list_dummy[int_len_2 - j - 1]);
    }
    
    // [list_in_1 X elem_2[0], 0, 0, 0, 0, 0]
    // [0, list_in_1 X elem_2[1], 0, 0, 0, 0]
    // [0, 0, list_in_1 X elem_2[2], 0, 0, 0]
    // [0, 0, 0, list_in_1 X elem_2[3], 0, 0]
    // [0, 0, 0, 0, list_in_1 X elem_2[4], 0]
    // [0, 0, 0, 0, 0, list_in_1 X elem_2[5]]
    

    let list_out = new Array();
    for (let i = 0; i < list_for_sum[0].length; i++){
        let sum_out = 0;
        for (let j = 0; j < int_len_2; j++){
            sum_out += list_for_sum[j][i];
        }
        list_out[i] = sum_out;
    }

    // [1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]
    return list_out;
}

main();

VBS
Option Explicit


Sub main()
    Const c_int_side_dice = 6  'сколько граней у кубика
    Const c_int_dice_number = 100  'кол-во кубиков
    Const c_int_number_to_find = 200  'число, вероятность выпадения которого хотим найти
    Dim probability
    probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice)
    MsgBox probability
End Sub


' собственно поиск вероятности определённого значения
Function dice_probability(int_dice_number, int_number_to_find, c_int_side_dice)
    If int_number_to_find >= int_dice_number And int_number_to_find <= c_int_side_dice * int_dice_number Then
        ReDim list_values(int_dice_number * (c_int_side_dice - 1))
        Dim i, j
        i = 0
        For j = int_dice_number To c_int_side_dice * int_dice_number
            list_values(i) = j
            i = i + 1
        Next
		Dim list_interm_probability()
		interm_probabilities c_int_side_dice, int_dice_number, list_interm_probability
		For i = 0 To int_dice_number * (c_int_side_dice - 1)
			If list_values(i) = int_number_to_find Then
				Exit For
			End If
		Next
		dice_probability = list_interm_probability(i) / (c_int_side_dice ^ int_dice_number)
    Else
        'задаваемое число выходит за рамки реально возможного диапазона значений
        dice_probability = 0.0
    End If
End Function


'возвращает список/массив: сколько раз встречается значение
Sub interm_probabilities(int_side_dice, int_pow, list_out)
    'На примере int_side_dice = 6, int_pow = 5
    '{
    '  1: [1, 1, 1, 1, 1, 1],
    '  2: [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1],
    '  4: [1, 4, 10, 20, 35, 56, 80, 104, 125, 140, 146, 140, 125, 104, 80, 56, 35, 20, 10, 4, 1]
    '  5: [1, 5, 15, 35, 70, 126, 205, 305, 420, 540, 651, 735, 780, 780, 735, 651, 540, 420, 305, 205, 126, 70, 35, 15, 5, 1]
    '}
    Dim j
    Dim list_interm_probability()
    ReDim list_interm_probability(int_side_dice - 1)
    For j = 0 To int_side_dice - 1
        list_interm_probability(j) = 1
    Next
    Dim dict_interm_probability
    Set dict_interm_probability = CreateObject("Scripting.Dictionary")
    dict_interm_probability.Add 1, list_interm_probability

    If int_pow = 0 Then
        MsgBox "Не поддерживается"
        Quit
    ElseIf int_pow <> 1 Then
        Dim list_to_do()
        map_todo list_to_do, int_pow
        For j = 0 To UBound(list_to_do, 2)
            'MsgBox list_to_do(0, j) & vbTab & list_to_do(1, j) & vbTab & list_to_do(2, j)
            multiply_cins_orig _
                dict_interm_probability.Item(list_to_do(0, j)), _
                dict_interm_probability.Item(list_to_do(1, j)), _
                list_out
            dict_interm_probability.Add list_to_do(2, j), list_out
            ' ArrOut_1 list_out
        Next
    End If
End Sub


'Как добраться до интересующего значения, используя x2/+nx для степеней
Sub map_todo(list_solution, int_wanted)
    'На примере int_wanted = 5
    'Степени "числа":
    '1
    '1 * 2 = 2 -> Array(1, 1, 2)
    '2 * 2 = 4 -> Array(2, 2, 4)
    '4 + 1 = 5 -> Array(4, 1, 5)

    Dim int_current_id
    Dim int_sum
    Dim b_ascending
    Dim i
    
    int_current_id = 1
    int_sum = 1
    b_ascending = True
    i = -1

    Do
        If b_ascending And 2 * int_current_id <= int_wanted Then
            i = i + 1
            ReDim Preserve list_solution(2, i)
            list_solution(0, i) = int_current_id
            list_solution(1, i) = int_current_id
            list_solution(2, i) = 2 * int_current_id
            int_current_id = 2 * int_current_id
            int_sum = int_current_id
        ElseIf b_ascending And 2 * int_current_id > int_wanted Then
            b_ascending = False
            int_sum = int_current_id
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        ElseIf Not b_ascending And int_sum + int_current_id <= int_wanted Then
            i = i + 1
            ReDim Preserve list_solution(2, i)
            list_solution(0, i) = int_sum
            list_solution(1, i) = int_current_id
            list_solution(2, i) = int_sum + int_current_id
            int_sum = int_sum + int_current_id
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        ElseIf Not b_ascending And int_sum + int_current_id > int_wanted Then
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        End If
    Loop Until (int_sum = int_wanted)
End Sub


' "умножение" в столбик двух массивов/списков
Sub multiply_cins_orig(list_in_1, list_in_2, list_in)
    Dim int_len_1
    Dim int_len_2
    int_len_1 = Ubound(list_in_1, 1)
    int_len_2 = Ubound(list_in_2, 1)

    Dim list_for_sum()
    ReDim list_for_sum(int_len_2, int_len_1 + int_len_2)
    Dim i, j, k, n
    For i = 0 To int_len_2
        j = 0
        For n = 0 To int_len_2
            If i = n Then
                For k = 0 To int_len_1
                    list_for_sum(i, j) = list_in_1(k) * list_in_2(n)
                    j = j + 1
                Next
            Else
                list_for_sum(i, j) = 0
                j = j + 1
            End If
        Next
    Next
    '[list_in_1 X elem_2[0], 0, 0, 0, 0, 0]
    '[0, list_in_1 X elem_2[1], 0, 0, 0, 0]
    '[0, 0, list_in_1 X elem_2[2], 0, 0, 0]
    '[0, 0, 0, list_in_1 X elem_2[3], 0, 0]
    '[0, 0, 0, 0, list_in_1 X elem_2[4], 0]
    '[0, 0, 0, 0, 0, list_in_1 X elem_2[5]]

    'ArrOut_2 list_for_sum
    Erase list_in
    ReDim list_in(int_len_1 + int_len_2)
    Dim sum_out
    For j = 0 To int_len_1 + int_len_2
        sum_out = 0
        For i = 0 To int_len_2
            sum_out = sum_out + list_for_sum(i, j)
        Next
        list_in(j) = sum_out
    Next
    ' [1, 3, 6, 10, 15, 21, 25, 27, 27, 25, 21, 15, 10, 6, 3, 1]
    'ArrOut_1 list_in
End Sub


'==================================================
'<Additional_MsgBox_For_Arrays>
Sub ArrOut_1(arr_in)
    Dim str_out
    Dim i
    For i = 0 To UBound(arr_in)
        If i = 0 Then
            str_out = arr_in(i)
        Else
            str_out = str_out & " " & arr_in(i)
        End If
    Next
    MsgBox str_out
End Sub

Sub ArrOut_2(arr_in)
    Dim str_out
    Dim i, j
    For i = 0 To UBound(arr_in, 1)
        For j = 0 To UBound(arr_in, 2)
            If i = 0 And j = 0 Then
                str_out = arr_in(i, j)
            ElseIf j = 0 Then
                str_out = str_out & vbNewLine & arr_in(i, j)
            Else
                str_out = str_out & " " & arr_in(i, j)
            End If
        Next
    Next
    MsgBox str_out
End Sub
'</Additional_MsgBox_For_Arrays>
'==================================================

main

Визуализация алгоритма, II попытка

Если быть достаточно честным, то из 3-х выложенных скриптов именно до 1000-го кубика может добраться только Python (без использования библиотеки numpy), а JavaScript и VBS выпадают в ошибку переполнение переменной. Предлагаю сделать небольшую хитрость: считать сразу вероятность выпадения внутри операции свёртки последовательностей / "умножения в столбик", вместо только делимого. Т.е. на вход сразу подавать последовательность [1/6 1/6 1/6 1/6 1/6 1/6] вместо [1 1 1 1 1 1] и, следовательно, на выходе всех операций свёртки последовательностей / "умножения в столбик" мы получим последовательность / массив / список вероятностей.

Python. Пример. Свёртка последовательностей [1/6 1/6 1/6 1/6 1/6 1/6]

Понимаю, что дроби проверять - дело не благодарное, следовательно добавляю степень 6 ** n * ... - делитель вероятности. Это сделано чисто для упрощения проверки.

# -*- coding: utf-8 -*-

import numpy

convolve_out = numpy.convolve([1 / 6] * 6, [1 / 6] * 6)
# [1 2 3 4 5 6 5 4 3 2 1]
print(6 ** 2 * convolve_out)

convolve_out = numpy.convolve(convolve_out, convolve_out)
# [ 1 4 10 20 35 56 80 104 125 140 146 140 125 104 80 56 35 20 10 4 1]
print(6 ** 4 * convolve_out )

convolve_out = numpy.convolve(convolve_out, [1 / 6] * 6)
# [ 1 5 15 35 70 126 205 305 420 540 651 735 780 780 735 651 540 420 305 205 126 70 35 15 5 1]
print(6 ** 5 * convolve_out)

Скрипты, II попытка

До 1000-го кубика добираются все. Простор для оптимизаций оставляю читателям.

Python
# -*- coding: utf-8 -*-
# import numpy  # <можно_использовать_numpy>

def main():
    c_int_side_dice: int = 6  # сколько граней у кубика
    c_int_dice_number: int = 1000  # кол-во кубиков
    c_int_number_to_find: int = 2000  # число, вероятность выпадения которого хотим найти
    probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice)
    print(probability)


# собственно поиск вероятности определённого значения
def dice_probability(int_dice_number: int, int_number_to_find: int, c_int_side_dice: int) -> float:
    if int_number_to_find >= int_dice_number and int_number_to_find <= c_int_side_dice * int_dice_number:
        list_values: list[int] = [i for i in range(int_dice_number, c_int_side_dice * int_dice_number + 1)]
        list_probability = get_probabilities(c_int_side_dice, int_dice_number)

        for i in range(len(list_values)):
            if list_values[i] == int_number_to_find:
                float_out: float = list_probability[i]
                break
        return float_out
    else:
        # задаваемое число выходит за рамки реально возможного диапазона значений
        return 0.0


# возвращает список/массив: вероятности выадения
def get_probabilities(int_side_dice: int, int_pow: int) -> list[float]:
    """
    На примере int_side_dice = 6, int_pow = 5
    {
      1: [1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6],
      2: [1 / 36, 2 / 36, 3 / 36, 4 / 36, 5 / 36, 6 / 36, 5 / 36, 4 / 36, 3 / 36, 2 / 36, 1 / 36],
      4: [1 / 1296, 4 / 1296, 10 / 1296, 20 / 1296, 35 / 1296, 56 / 1296, 80 / 1296, 104 / 1296, 125 / 1296, 140 / 1296, 146 / 1296, 140 / 1296, 125 / 1296, 104 / 1296, 80 / 1296, 56 / 1296, 35 / 1296, 20 / 1296, 10 / 1296, 4 / 1296, 1 / 1296]
      5: [1 / 7776, 5 / 7776, 15 / 7776, 35 / 7776, 70 / 7776, 126 / 7776, 205 / 7776, 305 / 7776, 420 / 7776, 540 / 7776, 651 / 7776, 735 / 7776, 780 / 7776, 780 / 7776, 735 / 7776, 651 / 7776, 540 / 7776, 420 / 7776, 305 / 7776, 205 / 7776, 126 / 7776, 70 / 7776, 35 / 7776, 15 / 7776, 5 / 7776, 1 / 7776]
    }
    """
    dict_interm_probability = {1: [1 / int_side_dice] * int_side_dice}
    if int_pow == 0:
        print("Не поддерживается")
        quit()
    elif int_pow != 1:
        list_to_do = map_todo(int_pow) 
        for elem in list_to_do:
            dict_interm_probability[elem[2]] = multiply_cins_orig(dict_interm_probability[elem[0]], dict_interm_probability[elem[1]])
            # dict_interm_probability[elem[2]] = numpy.convolve(dict_interm_probability[elem[0]], dict_interm_probability[elem[1]])  # <можно_использовать_numpy> и не использовать multiply_cins_orig()
    return dict_interm_probability[int_pow]


# Как добраться до интересующего значения, используя x2/+nx для степеней
def map_todo(int_wanted: int) -> list[tuple[int, int, int]]:
    """
    На примере int_wanted = 5
    Степени "числа":
    1
    1 * 2 = 2 -> tuple(1, 1, 2)
    2 * 2 = 4 -> tuple(2, 2, 4)
    4 + 1 = 5 -> tuple(4, 1, 5)
    """

    int_current_id: int = 1
    int_sum: int = 1
    b_ascending: bool = True
    list_solution: list[tuple[int, int, int]] = []

    while True:
        if int_sum == int_wanted:
            break
        elif b_ascending and 2 * int_current_id <= int_wanted:
            list_solution.append(  # mult_1, mult_2, result
                (int_current_id, int_current_id, 2 * int_current_id)
            )
            int_current_id = 2 * int_current_id
            int_sum = int_current_id
        elif b_ascending and 2 * int_current_id > int_wanted:
            b_ascending = False
            int_sum = int_current_id
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
        elif not b_ascending and int_sum + int_current_id <= int_wanted:
            list_solution.append(  # mult_1, mult_2, result
                (int_sum, int_current_id, int_sum + int_current_id)
            )
            int_sum = int_sum + int_current_id
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
        elif not b_ascending and int_sum + int_current_id > int_wanted:
            int_current_id = int(int_current_id / 2)  # чтобы возвращал именно integer
    return list_solution


# "умножение" в столбик двух массивов/списков
def multiply_cins_orig(list_in_1: list[int], list_in_2: list[int]) -> list[int]:
    int_len_2: int = len(list_in_2)
    list_dummy: list[list[int]] = []
    for i in range(int_len_2):
        list_dummy.append([0] * i)  # [], [0], [0, 0], [0, 0, 0] ...

    list_for_sum: list[list[int]] = []
    i: int = -1
    for elem_2 in list_in_2:
        i += 1
        list_interm: list[int] = [elem_1 * elem_2 for elem_1 in list_in_1]
        list_for_sum.append(list_dummy[i] + list_interm + list_dummy[int_len_2 - i - 1])

    """
    [list_in_1 X list_in_2[0], 0, 0, 0, 0, 0]
    [0, list_in_1 X list_in_2[1], 0, 0, 0, 0]
    [0, 0, list_in_1 X list_in_2[2], 0, 0, 0]
    [0, 0, 0, list_in_1 X list_in_2[3], 0, 0]
    [0, 0, 0, 0, list_in_1 X list_in_2[4], 0]
    [0, 0, 0, 0, 0, list_in_1 X list_in_2[5]]
    """

    list_out: list[int] = []
    for i in range(len(list_for_sum[0])):
        sum_out: int = 0
        for j in range(int_len_2):
            sum_out += list_for_sum[j][i]
        list_out.append(sum_out)
    """
    [1 / 216, 3 / 216, 6 / 216, 10 / 216, 15 / 216, 21 / 216, 25 / 216, 27 / 216, 27 / 216, 25 / 216, 21 / 216, 15 / 216, 10 / 216, 6 / 216, 3 / 216, 1 / 216]
    """
    return list_out


main()

JavaScript
function main(){
    const c_int_side_dice = 6;  // сколько граней у кубика
    const c_int_dice_number = 1000; // кол-во кубиков
    const c_int_number_to_find = 2000; // число, вероятность выпадения которого хотим найти
    let probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice);
    console.log(probability);
}


// собственно поиск вероятности определённого значения
function dice_probability(int_dice_number, int_number_to_find, c_int_side_dice){
    if (int_number_to_find >= int_dice_number && int_number_to_find <= c_int_side_dice * int_dice_number){
        let list_values = new Array();
        let i = 0;
        for (let j = int_dice_number; j <= c_int_side_dice * int_dice_number; j++){
            list_values[i] = j;
            i++;
        }
        let list_probability = get_probabilities(c_int_side_dice, int_dice_number);
        let float_out;
        for (let i = 0; i <= list_values.length; i++){
            if (list_values[i] == int_number_to_find){
                float_out = list_probability[i];
                break;
            }
        }
        return float_out;
    } else {
        // задаваемое число выходит за рамки реально возможного диапазона значений
        return 0.0;
    }
}

// возвращает список/массив: вероятности выадения
function get_probabilities(int_side_dice, int_pow){
    // На примере int_side_dice = 6, int_pow = 5
    // {
    //   1: [1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6],
    //   2: [1 / 36, 2 / 36, 3 / 36, 4 / 36, 5 / 36, 6 / 36, 5 / 36, 4 / 36, 3 / 36, 2 / 36, 1 / 36],
    //   4: [1 / 1296, 4 / 1296, 10 / 1296, 20 / 1296, 35 / 1296, 56 / 1296, 80 / 1296, 104 / 1296, 125 / 1296, 140 / 1296, 146 / 1296, 140 / 1296, 125 / 1296, 104 / 1296, 80 / 1296, 56 / 1296, 35 / 1296, 20 / 1296, 10 / 1296, 4 / 1296, 1 / 1296]
    //   5: [1 / 7776, 5 / 7776, 15 / 7776, 35 / 7776, 70 / 7776, 126 / 7776, 205 / 7776, 305 / 7776, 420 / 7776, 540 / 7776, 651 / 7776, 735 / 7776, 780 / 7776, 780 / 7776, 735 / 7776, 651 / 7776, 540 / 7776, 420 / 7776, 305 / 7776, 205 / 7776, 126 / 7776, 70 / 7776, 35 / 7776, 15 / 7776, 5 / 7776, 1 / 7776]
    // }

    let dict_interm_probability = {1: Array(int_side_dice).fill(1 / int_side_dice)};
    if (int_pow == 0){
        console.log("Не поддерживается");
        return;
    } else if (int_pow != 1){
        let list_to_do = map_todo(int_pow); 
        for (let i = 0; i < list_to_do.length; i++){
            dict_interm_probability[list_to_do[i][2]] = multiply_cins_orig(dict_interm_probability[list_to_do[i][0]], dict_interm_probability[list_to_do[i][1]]);
        }
    }
    return dict_interm_probability[int_pow];
}


// Как добраться до интересующего значения, используя x2/+nx для степеней
function map_todo(int_wanted){
    // На примере int_wanted = 5
    // Степени "числа":
    // 1
    // 1 * 2 = 2 -> Array(1, 1, 2)
    // 2 * 2 = 4 -> Array(2, 2, 4)
    // 4 + 1 = 5 -> Array(4, 1, 5)

    let int_current_id = 1;
    let int_sum = 1;
    let b_ascending = true;
    let list_solution = new Array();
    let i = 0;
    while (true){
        if (int_sum == int_wanted){
            break;
        } else if (b_ascending && 2 * int_current_id <= int_wanted){
            list_solution[i] = [int_current_id, int_current_id, 2 * int_current_id];  // mult_1, mult_2, result
            i++;
            int_current_id = 2 * int_current_id;
            int_sum = int_current_id;
        } else if (b_ascending && 2 * int_current_id > int_wanted){
            b_ascending = false;
            int_sum = int_current_id;
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        } else if (!b_ascending && int_sum + int_current_id <= int_wanted){
            list_solution[i] = [int_sum, int_current_id, int_sum + int_current_id];  // mult_1, mult_2, result
            i++;
            int_sum = int_sum + int_current_id;
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        } else if (!b_ascending && int_sum + int_current_id > int_wanted){
            int_current_id = Math.ceil(int_current_id / 2);  // чтобы возвращал именно integer
        }
    }
    return list_solution;
}

// "умножение" в столбик двух массивов/списков
function multiply_cins_orig(list_in_1, list_in_2){
    let int_len_1 = list_in_1.length;
    let int_len_2 = list_in_2.length;
    
    let list_dummy = new Array();
    for (let j = 0; j < int_len_2; j++){
        list_dummy[j] = Array(j).fill(0);  // [], [0], [0, 0], [0, 0, 0] ...
    }

    let list_for_sum = new Array();
    for (let j = 0; j < int_len_2; j++){
        let list_interm = new Array();
        for (let i = 0; i < int_len_1; i++){
            list_interm[i] = list_in_1[i] * list_in_2[j]
        }
        list_for_sum[j] = list_dummy[j].concat(list_interm, list_dummy[int_len_2 - j - 1]);
    }
    
    // [list_in_1 X list_in_2[0], 0, 0, 0, 0, 0]
    // [0, list_in_1 X list_in_2[1], 0, 0, 0, 0]
    // [0, 0, list_in_1 X list_in_2[2], 0, 0, 0]
    // [0, 0, 0, list_in_1 X list_in_2[3], 0, 0]
    // [0, 0, 0, 0, list_in_1 X list_in_2[4], 0]
    // [0, 0, 0, 0, 0, list_in_1 X list_in_2[5]]
    

    let list_out = new Array();
    for (let i = 0; i < list_for_sum[0].length; i++){
        let sum_out = 0;
        for (let j = 0; j < int_len_2; j++){
            sum_out += list_for_sum[j][i];
        }
        list_out[i] = sum_out;
    }

    // [1 / 216, 3 / 216, 6 / 216, 10 / 216, 15 / 216, 21 / 216, 25 / 216, 27 / 216, 27 / 216, 25 / 216, 21 / 216, 15 / 216, 10 / 216, 6 / 216, 3 / 216, 1 / 216]
    return list_out;
}

main();

VBS
Option Explicit


Sub main()
    Const c_int_side_dice = 6  'сколько граней у кубика
    Const c_int_dice_number = 1000  'кол-во кубиков
    Const c_int_number_to_find = 2000  'число, вероятность выпадения которого хотим найти
    Dim probability
    probability = dice_probability(c_int_dice_number, c_int_number_to_find, c_int_side_dice)
    MsgBox probability
End Sub


' собственно поиск вероятности определённого значения
Function dice_probability(int_dice_number, int_number_to_find, c_int_side_dice)
    If int_number_to_find >= int_dice_number And int_number_to_find <= c_int_side_dice * int_dice_number Then
        ReDim list_values(int_dice_number * (c_int_side_dice - 1))
        Dim i, j
        i = 0
        For j = int_dice_number To c_int_side_dice * int_dice_number
            list_values(i) = j
            i = i + 1
        Next
        Dim list_probability()
        get_probabilities c_int_side_dice, int_dice_number, list_probability
        For i = 0 To int_dice_number * (c_int_side_dice - 1)
            If list_values(i) = int_number_to_find Then
                Exit For
            End If
        Next
        dice_probability = list_probability(i)
    Else
        'задаваемое число выходит за рамки реально возможного диапазона значений
        dice_probability = 0.0
    End If
End Function


'возвращает список/массив: вероятности выадения
Sub get_probabilities(int_side_dice, int_pow, list_out)
    'На примере int_side_dice = 6, int_pow = 5
    '{
    '  1: [1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6],
    '  2: [1 / 36, 2 / 36, 3 / 36, 4 / 36, 5 / 36, 6 / 36, 5 / 36, 4 / 36, 3 / 36, 2 / 36, 1 / 36],
    '  4: [1 / 1296, 4 / 1296, 10 / 1296, 20 / 1296, 35 / 1296, 56 / 1296, 80 / 1296, 104 / 1296, 125 / 1296, 140 / 1296, 146 / 1296, 140 / 1296, 125 / 1296, 104 / 1296, 80 / 1296, 56 / 1296, 35 / 1296, 20 / 1296, 10 / 1296, 4 / 1296, 1 / 1296]
    '  5: [1 / 7776, 5 / 7776, 15 / 7776, 35 / 7776, 70 / 7776, 126 / 7776, 205 / 7776, 305 / 7776, 420 / 7776, 540 / 7776, 651 / 7776, 735 / 7776, 780 / 7776, 780 / 7776, 735 / 7776, 651 / 7776, 540 / 7776, 420 / 7776, 305 / 7776, 205 / 7776, 126 / 7776, 70 / 7776, 35 / 7776, 15 / 7776, 5 / 7776, 1 / 7776]
    '}
    Dim j
    Dim list_probability()
    ReDim list_probability(int_side_dice - 1)
    For j = 0 To int_side_dice - 1
        list_probability(j) = 1 / int_side_dice
    Next
    Dim dict_interm_probability
    Set dict_interm_probability = CreateObject("Scripting.Dictionary")
    dict_interm_probability.Add 1, list_probability

    If int_pow = 0 Then
        MsgBox "Не поддерживается"
        Quit
    ElseIf int_pow <> 1 Then
        Dim list_to_do()
        map_todo list_to_do, int_pow
        For j = 0 To UBound(list_to_do, 2)
            'MsgBox list_to_do(0, j) & vbTab & list_to_do(1, j) & vbTab & list_to_do(2, j)
            multiply_cins_orig _
                dict_interm_probability.Item(list_to_do(0, j)), _
                dict_interm_probability.Item(list_to_do(1, j)), _
                list_out
            dict_interm_probability.Add list_to_do(2, j), list_out
            ' ArrOut_1 list_out
        Next
    End If
End Sub


'Как добраться до интересующего значения, используя x2/+nx для степеней
Sub map_todo(list_solution, int_wanted)
    'На примере int_wanted = 5
    'Степени "числа":
    '1
    '1 * 2 = 2 -> Array(1, 1, 2)
    '2 * 2 = 4 -> Array(2, 2, 4)
    '4 + 1 = 5 -> Array(4, 1, 5)

    Dim int_current_id
    Dim int_sum
    Dim b_ascending
    Dim i
    
    int_current_id = 1
    int_sum = 1
    b_ascending = True
    i = -1

    Do
        If b_ascending And 2 * int_current_id <= int_wanted Then
            i = i + 1
            ReDim Preserve list_solution(2, i)
            list_solution(0, i) = int_current_id
            list_solution(1, i) = int_current_id
            list_solution(2, i) = 2 * int_current_id
            int_current_id = 2 * int_current_id
            int_sum = int_current_id
        ElseIf b_ascending And 2 * int_current_id > int_wanted Then
            b_ascending = False
            int_sum = int_current_id
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        ElseIf Not b_ascending And int_sum + int_current_id <= int_wanted Then
            i = i + 1
            ReDim Preserve list_solution(2, i)
            list_solution(0, i) = int_sum
            list_solution(1, i) = int_current_id
            list_solution(2, i) = int_sum + int_current_id
            int_sum = int_sum + int_current_id
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        ElseIf Not b_ascending And int_sum + int_current_id > int_wanted Then
            int_current_id = CInt(int_current_id / 2)  'чтобы возвращал именно integer
        End If
    Loop Until (int_sum = int_wanted)
End Sub


' "умножение" в столбик двух массивов/списков
Sub multiply_cins_orig(list_in_1, list_in_2, list_in)
    Dim int_len_1
    Dim int_len_2
    int_len_1 = Ubound(list_in_1, 1)
    int_len_2 = Ubound(list_in_2, 1)

    Dim list_for_sum()
    ReDim list_for_sum(int_len_2, int_len_1 + int_len_2)
    Dim i, j, k, n
    For i = 0 To int_len_2
        j = 0
        For n = 0 To int_len_2
            If i = n Then
                For k = 0 To int_len_1
                    list_for_sum(i, j) = list_in_1(k) * list_in_2(n)
                    j = j + 1
                Next
            Else
                list_for_sum(i, j) = 0
                j = j + 1
            End If
        Next
    Next
    '[list_in_1 X list_in_2[0], 0, 0, 0, 0, 0]
    '[0, list_in_1 X list_in_2[1], 0, 0, 0, 0]
    '[0, 0, list_in_1 X list_in_2[2], 0, 0, 0]
    '[0, 0, 0, list_in_1 X list_in_2[3], 0, 0]
    '[0, 0, 0, 0, list_in_1 X list_in_2[4], 0]
    '[0, 0, 0, 0, 0, list_in_1 X list_in_2[5]]

    'ArrOut_2 list_for_sum
    Erase list_in
    ReDim list_in(int_len_1 + int_len_2)
    Dim sum_out
    For j = 0 To int_len_1 + int_len_2
        sum_out = 0
        For i = 0 To int_len_2
            sum_out = sum_out + list_for_sum(i, j)
        Next
        list_in(j) = sum_out
    Next
    ' [1 / 216, 3 / 216, 6 / 216, 10 / 216, 15 / 216, 21 / 216, 25 / 216, 27 / 216, 27 / 216, 25 / 216, 21 / 216, 15 / 216, 10 / 216, 6 / 216, 3 / 216, 1 / 216]
    'ArrOut_1 list_in
End Sub


'==================================================
'<Additional_MsgBox_For_Arrays>
Sub ArrOut_1(arr_in)
    Dim str_out
    Dim i
    For i = 0 To UBound(arr_in)
        If i = 0 Then
            str_out = arr_in(i)
        Else
            str_out = str_out & " " & arr_in(i)
        End If
    Next
    MsgBox str_out
End Sub

Sub ArrOut_2(arr_in)
    Dim str_out
    Dim i, j
    For i = 0 To UBound(arr_in, 1)
        For j = 0 To UBound(arr_in, 2)
            If i = 0 And j = 0 Then
                str_out = arr_in(i, j)
            ElseIf j = 0 Then
                str_out = str_out & vbNewLine & arr_in(i, j)
            Else
                str_out = str_out & " " & arr_in(i, j)
            End If
        Next
    Next
    MsgBox str_out
End Sub
'</Additional_MsgBox_For_Arrays>
'==================================================

main

Пару слов о проверке

Проверку и перепроверку чьих бы то ни было слов всегда приветствую.

Однако, отмечу, что на текущий момент кроме как эмпирического (т.е. обычного сравнения результата работы описанного в текущей статье алгоритма с работой алгоритма в предыдущей статье или обычного расчёта “в лоб”) метода проверки я не располагаю каким-либо иным доказательством своей правоты, которое бы сочетало как доступность восприятия так и наглядность.

Следовательно:

Если Вы достаточно доверяете предыдущей статье — то можно сверять результат работы алгоритма в текущей статье с алгоритмом, описанным в предыдущей статье.

Если не доверяете предыдущей статье, тогда могу предложить следующее: генератор SQL запросов, считающих в лоб кубики, суммы их выпадений и их вероятности.

Python для проверок в лоб
# -*- coding: utf-8 -*-

import sqlite3
import re

def main() -> None:
    c_int_side_dice: int = 6  # сколько граней у кубика
    c_int_dice_number: int = 6  # кол-во кубиков

    str_query = select_values_and_interm_probabilities(c_int_side_dice, c_int_dice_number)
    if True:
        # Просмотр SQL кода
        print(str_query)
    else:
        # Прогон SQL запроса
        conn = sqlite3.connect(":memory:")
        cursor = conn.cursor()

        cursor.execute(str_query)
        return_select(cursor)

        cursor.close()
        conn.close()


def select_values_and_interm_probabilities(int_side_dice: int, int_dice_number: int) -> str:
    str_sub_query_1: str = """
    -- заводим значения сторон кубика
    WITH RECURSIVE step_01_insert (dice) AS (
        SELECT 1 AS dice
        UNION ALL
        SELECT dice + 1 AS dice 
        FROM step_01_insert
        WHERE dice < {}  -- сколько граней у кубика
    )
    """.format(int_side_dice)
    list_sub_query_1: list[str] = []
    list_sub_query_2: list[str] = []
    list_sub_query_3: list[str] = []
    for i in range(int_dice_number):
        list_sub_query_1.append("T{}.dice AS dice_{}".format(i, i))
        list_sub_query_2.append("T{}.dice".format(i))
        list_sub_query_3.append("step_01_insert AS T{}".format(i))

    str_sub_query_2: str = "\n".join([
        "-- генерируем все возможные ситуации для {}-х кубиков".format(int_dice_number),
        ", step_02_spawn AS (",
            "SELECT",
            "\n, ".join(list_sub_query_1),
            ", " + " + ".join(list_sub_query_2) + " AS dice_sum  -- Значения (сумма выпавших костей)",
            "FROM",
            "\n, ".join(list_sub_query_3),
        ")"
    ])
    del list_sub_query_1, list_sub_query_2, list_sub_query_3

    str_sub_query_3: str = """
    -- считаем в лоб, сколько раз встречается значение
    , step_03_dividend (dice_sum, dividend) AS (
        SELECT dice_sum  -- Значения (сумма выпавших костей)
        , COUNT(1) AS dividend  -- Сколько раз встречается значение
        FROM step_02_spawn
        GROUP BY dice_sum
    )
    , step_04_divisor(divisor) AS (
        SELECT SUM(dividend) AS divisor
        FROM step_03_dividend
    )
    SELECT T1.dice_sum  -- Значения (сумма выпавших костей)
    , T1.dividend  -- Сколько раз встречается значение
    , CAST(T1.dividend AS REAL) / CAST(T2.divisor AS REAL) AS probability  -- Вероятность
    FROM step_03_dividend AS T1
    , step_04_divisor AS T2
    ORDER BY T1.dice_sum;
    """
    return lazy_prety_print(str_sub_query_1 + str_sub_query_2 + str_sub_query_3)


def lazy_prety_print(str_in: str) -> str:
    list_line: list[str] = []
    str_offset: str = ""
    for str_line_1 in str_in.split("\n"):
        str_line_2 = re.sub(r"^\s+", "", str_line_1)
        if len(str_line_2) > 0 and str_line_2[0] == ")":
            str_offset = str_offset[:-4]
        list_line.append(str_offset + str_line_2)
        if len(str_line_2) > 0 and str_line_2[-1] == "(":
            str_offset = str_offset + "    "
    return "\n".join(list_line)


def return_select(cursor: sqlite3.Cursor) -> None:
    column_list = []

    for column in cursor.description:
        column_list.append(column[0])

    print("\t".join(column_list))

    rows = cursor.fetchall()

    for row in rows:
        column_list = []
        for row_column in row:
            column_list.append(str(row_column))
        print("\t".join(column_list))


if __name__ == "__main__":
    main()

Результаты работы данного скрипта приведены ниже:

SQL для 3-х кубиков
-- заводим значения сторон кубика
WITH RECURSIVE step_01_insert (dice) AS (
    SELECT 1 AS dice
    UNION ALL
    SELECT dice + 1 AS dice 
    FROM step_01_insert
    WHERE dice < 6  -- сколько граней у кубика
)
-- генерируем все возможные ситуации для 3-х кубиков
, step_02_spawn AS (
    SELECT
    T0.dice AS dice_0
    , T1.dice AS dice_1
    , T2.dice AS dice_2
    , T0.dice + T1.dice + T2.dice AS dice_sum  -- Значения (сумма выпавших костей)
    FROM
    step_01_insert AS T0
    , step_01_insert AS T1
    , step_01_insert AS T2
)
-- считаем в лоб, сколько раз встречается значение
, step_03_dividend (dice_sum, dividend) AS (
    SELECT dice_sum  -- Значения (сумма выпавших костей)
    , COUNT(1) AS dividend  -- Сколько раз встречается значение
    FROM step_02_spawn
    GROUP BY dice_sum
)
, step_04_divisor(divisor) AS (
    SELECT SUM(dividend) AS divisor
    FROM step_03_dividend
)
SELECT T1.dice_sum  -- Значения (сумма выпавших костей)
, T1.dividend  -- Сколько раз встречается значение
, CAST(T1.dividend AS REAL) / CAST(T2.divisor AS REAL) AS probability  -- Вероятность
FROM step_03_dividend AS T1
, step_04_divisor AS T2
ORDER BY T1.dice_sum;

dice_sum

dividend

probability

3

1

0.004629629629629629

4

3

0.013888888888888888

5

6

0.027777777777777776

6

10

0.046296296296296294

7

15

0.06944444444444445

8

21

0.09722222222222222

9

25

0.11574074074074074

10

27

0.125

11

27

0.125

12

25

0.11574074074074074

13

21

0.09722222222222222

14

15

0.06944444444444445

15

10

0.046296296296296294

16

6

0.027777777777777776

17

3

0.013888888888888888

18

1

0.004629629629629629

SQL для 4-х кубиков
-- заводим значения сторон кубика
WITH RECURSIVE step_01_insert (dice) AS (
    SELECT 1 AS dice
    UNION ALL
    SELECT dice + 1 AS dice 
    FROM step_01_insert
    WHERE dice < 6  -- сколько граней у кубика
)
-- генерируем все возможные ситуации для 4-х кубиков
, step_02_spawn AS (
    SELECT
    T0.dice AS dice_0
    , T1.dice AS dice_1
    , T2.dice AS dice_2
    , T3.dice AS dice_3
    , T0.dice + T1.dice + T2.dice + T3.dice AS dice_sum  -- Значения (сумма выпавших костей)
    FROM
    step_01_insert AS T0
    , step_01_insert AS T1
    , step_01_insert AS T2
    , step_01_insert AS T3
)
-- считаем в лоб, сколько раз встречается значение
, step_03_dividend (dice_sum, dividend) AS (
    SELECT dice_sum  -- Значения (сумма выпавших костей)
    , COUNT(1) AS dividend  -- Сколько раз встречается значение
    FROM step_02_spawn
    GROUP BY dice_sum
)
, step_04_divisor(divisor) AS (
    SELECT SUM(dividend) AS divisor
    FROM step_03_dividend
)
SELECT T1.dice_sum  -- Значения (сумма выпавших костей)
, T1.dividend  -- Сколько раз встречается значение
, CAST(T1.dividend AS REAL) / CAST(T2.divisor AS REAL) AS probability  -- Вероятность
FROM step_03_dividend AS T1
, step_04_divisor AS T2
ORDER BY T1.dice_sum;

dice_sum

dividend

probability

4

1

0.0007716049382716049

5

4

0.0030864197530864196

6

10

0.007716049382716049

7

20

0.015432098765432098

8

35

0.02700617283950617

9

56

0.043209876543209874

10

80

0.06172839506172839

11

104

0.08024691358024691

12

125

0.09645061728395062

13

140

0.10802469135802469

14

146

0.11265432098765432

15

140

0.10802469135802469

16

125

0.09645061728395062

17

104

0.08024691358024691

18

80

0.06172839506172839

19

56

0.043209876543209874

20

35

0.02700617283950617

21

20

0.015432098765432098

22

10

0.007716049382716049

23

4

0.0030864197530864196

24

1

0.0007716049382716049

SQL для 5-х кубиков
-- заводим значения сторон кубика
WITH RECURSIVE step_01_insert (dice) AS (
    SELECT 1 AS dice
    UNION ALL
    SELECT dice + 1 AS dice 
    FROM step_01_insert
    WHERE dice < 6  -- сколько граней у кубика
)
-- генерируем все возможные ситуации для 5-х кубиков
, step_02_spawn AS (
    SELECT
    T0.dice AS dice_0
    , T1.dice AS dice_1
    , T2.dice AS dice_2
    , T3.dice AS dice_3
    , T4.dice AS dice_4
    , T0.dice + T1.dice + T2.dice + T3.dice + T4.dice AS dice_sum  -- Значения (сумма выпавших костей)
    FROM
    step_01_insert AS T0
    , step_01_insert AS T1
    , step_01_insert AS T2
    , step_01_insert AS T3
    , step_01_insert AS T4
)
-- считаем в лоб, сколько раз встречается значение
, step_03_dividend (dice_sum, dividend) AS (
    SELECT dice_sum  -- Значения (сумма выпавших костей)
    , COUNT(1) AS dividend  -- Сколько раз встречается значение
    FROM step_02_spawn
    GROUP BY dice_sum
)
, step_04_divisor(divisor) AS (
    SELECT SUM(dividend) AS divisor
    FROM step_03_dividend
)
SELECT T1.dice_sum  -- Значения (сумма выпавших костей)
, T1.dividend  -- Сколько раз встречается значение
, CAST(T1.dividend AS REAL) / CAST(T2.divisor AS REAL) AS probability  -- Вероятность
FROM step_03_dividend AS T1
, step_04_divisor AS T2
ORDER BY T1.dice_sum;

dice_sum

dividend

probability

5

1

0.0001286008230452675

6

5

0.0006430041152263374

7

15

0.0019290123456790122

8

35

0.0045010288065843625

9

70

0.009002057613168725

10

126

0.016203703703703703

11

205

0.026363168724279837

12

305

0.03922325102880658

13

420

0.05401234567901234

14

540

0.06944444444444445

15

651

0.08371913580246913

16

735

0.09452160493827161

17

780

0.10030864197530864

18

780

0.10030864197530864

19

735

0.09452160493827161

20

651

0.08371913580246913

21

540

0.06944444444444445

22

420

0.05401234567901234

23

305

0.03922325102880658

24

205

0.026363168724279837

25

126

0.016203703703703703

26

70

0.009002057613168725

27

35

0.0045010288065843625

28

15

0.0019290123456790122

29

5

0.0006430041152263374

30

1

0.0001286008230452675

SQL для 6-х кубиков
-- заводим значения сторон кубика
WITH RECURSIVE step_01_insert (dice) AS (
    SELECT 1 AS dice
    UNION ALL
    SELECT dice + 1 AS dice 
    FROM step_01_insert
    WHERE dice < 6  -- сколько граней у кубика
)
-- генерируем все возможные ситуации для 6-х кубиков
, step_02_spawn AS (
    SELECT
    T0.dice AS dice_0
    , T1.dice AS dice_1
    , T2.dice AS dice_2
    , T3.dice AS dice_3
    , T4.dice AS dice_4
    , T5.dice AS dice_5
    , T0.dice + T1.dice + T2.dice + T3.dice + T4.dice + T5.dice AS dice_sum  -- Значения (сумма выпавших костей)
    FROM
    step_01_insert AS T0
    , step_01_insert AS T1
    , step_01_insert AS T2
    , step_01_insert AS T3
    , step_01_insert AS T4
    , step_01_insert AS T5
)
-- считаем в лоб, сколько раз встречается значение
, step_03_dividend (dice_sum, dividend) AS (
    SELECT dice_sum  -- Значения (сумма выпавших костей)
    , COUNT(1) AS dividend  -- Сколько раз встречается значение
    FROM step_02_spawn
    GROUP BY dice_sum
)
, step_04_divisor(divisor) AS (
    SELECT SUM(dividend) AS divisor
    FROM step_03_dividend
)
SELECT T1.dice_sum  -- Значения (сумма выпавших костей)
, T1.dividend  -- Сколько раз встречается значение
, CAST(T1.dividend AS REAL) / CAST(T2.divisor AS REAL) AS probability  -- Вероятность
FROM step_03_dividend AS T1
, step_04_divisor AS T2
ORDER BY T1.dice_sum;

dice_sum

dividend

probability

6

1

2.143347050754458e-05

7

6

0.0001286008230452675

8

21

0.0004501028806584362

9

56

0.0012002743484224967

10

126

0.002700617283950617

11

252

0.005401234567901234

12

456

0.00977366255144033

13

756

0.016203703703703703

14

1161

0.02488425925925926

15

1666

0.03570816186556927

16

2247

0.048161008230452676

17

2856

0.061213991769547324

18

3431

0.07353823731138547

19

3906

0.08371913580246913

20

4221

0.09047067901234568

21

4332

0.09284979423868313

22

4221

0.09047067901234568

23

3906

0.08371913580246913

24

3431

0.07353823731138547

25

2856

0.061213991769547324

26

2247

0.048161008230452676

27

1666

0.03570816186556927

28

1161

0.02488425925925926

29

756

0.016203703703703703

30

456

0.00977366255144033

31

252

0.005401234567901234

32

126

0.002700617283950617

33

56

0.0012002743484224967

34

21

0.0004501028806584362

35

6

0.0001286008230452675

36

1

2.143347050754458e-05

Выводы

В данной статье мы познакомились:
- с операцией свёртка последовательностей
- как при помощи свёртки последовательностей считать вероятности выпадения кубиков
- как посчитать вероятности выпадения найти вероятность выпадения числа k, а именно суммы всех значений, выпавших для 1000 кубиков.