Рекурсивный алгоритм решения задачи Ханойская башня

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

void move(int i, int j, int d)

{

int m;

for(m = 0; m < d; m++) printf(" ");

printf("перемещение(%d,%d)\n", i, j);

}

void hanoy(int i, int j, int k, int d)

{

// для отладки

//int m ;

//for (m = 0; m < d; m++) printf(" ");

//printf("hanoy(%d,%d,%d)\n", i, j, k);

if (k == 1)

move(i, j, d);

else

{

hanoy(i, 6-i-j, k-1, d+1);

hanoy(i, j, 1, d+1);

hanoy(6-i-j, j, k-1, d+1);

}

}

int main()

{

int n = 4;

printf("Введите количество дисков : ");

scanf("%d",&n);

printf("\nХаной %d дисками :\n", n);

hanoy(1, 3, n, 0);

getch();

// system ("pause");

}

Результат выполнения программы

Введите количество дисков : 4

Ханой с 4 дисками :

перемещение(1,2)

перемещение(1,3)

перемещение(2,3)

перемещение(1,2)

перемещение(3,1)

перемещение(3,2)

перемещение(1,2)

перемещение(1,3)

перемещение(2,3)

перемещение(2,1)

перемещение(3,1)

перемещение(2,3)

перемещение(1,2)

перемещение(1,3)

перемещение(2,3)

Пример 13. Функция решение кубического уравнения с действительными коэффициентами методом Виета-Кардано. Корни могут быть комплексными.

Кубическое уравнение записывается в виде:

x3+a*x2+b*x+c=0.

Для нахождения его корней, в случае действительных коэффициентов, вначале вычисляются:

Q = (a2-3b)/9

R = (2a3-9ab+27c)/54.

Далее, если R2 < Q3, то уравнение имеет три действительных корня, вычисляющихся по формулам (Виета):

е = acos(R/sqrt(Q3))/3,

x1 = -2*sqrt(Q)cos(t)-a/3,

x2 = -2*sqrt(Q)cos(t+(2*pi/3))-a/3,

x3 = -2*sqrt(Q)cos(t-(2*pi/3))-a/3.

В том случае, когда R2 >= Q3, то действительных корней один (общий случай) или два (вырожденные случаи). Кроме действительного корня, имеется два комплексно-сопряженных. Для их нахождения вычисляются (формула Кардано):

A = -sign(R)[|R|+sqrt(R2-Q3)]1/3,

B = Q/A при A != 0 или B = 0 при A = 0.

Действительный корень будет:

x1 = (A+B)-a/3.

Комплексно-сопряженные корни:

x2,3 = -(A+B)/2-a/3 + i*sqrt(3)*(A-B)/2

В том случае, когда A = B, то комплексно-сопряженные корни вырождаются в действительный:

x2=-A-a/3.

Формулы Кардано и Виета требуют применения специальных функций, и в том случае, когда требуется провести большую серию вычислений корней кубического уравнения с не слишком сильно меняющимися коэффициентами, более быстрым алгоритмом является использование метода Ньютона или других итерационных методов (с нахождением начального приближения по формулам Кардано-Виета).

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

#define M_PI (3.141592653589793)

#define M_2PI (2.*M_PI)

/*

int Cubic(double *x,double a,double b,double c);

Параметры:

a, b, c - коэффициенты

x - массив решения (size 3).

На выходе:

3 действительных корня -> затем x ими заполняется;

1 действительный + 2 комплексных -> x[0] - действительный, x[1] действительная часть комплексных корней, x[2] - неотрицательная мнимая часть.

Результаты:

1 - 1 действительный + 2 комплексных;

2 - 1 действительный корень + мнимая часть комплексных корней, если 0 (т.е. 2 действительных корня).

3 - 3 действительных корня;

*/

int Cubic (double *x, double a, double b, double c)

{

double q, r, r2, q3;

q = (a * a - 3. * b) / 9.;

r = (a * (2. * a * a - 9. * b) + 27.*c) / 54.;

r2 = r * r;

q3 = q * q * q;

if (r2 < q3)

{

double t = acos(r / sqrt (q3));

a /= 3.;

q = -2. * sqrt (q);

x[0] = q * cos (t / 3.) - a;

x[1] = q * cos ((t + M_2PI) / 3.) - a;

x[2] = q * cos ((t - M_2PI) / 3.) - a;

return (3);

}

else

{

double aa, bb;

if (r <= 0.) r =- r;

aa = -pow (r + sqrt (r2 - q3), 1. / 3.);

if(aa != 0.) bb = q / aa;

else bb = 0.;

a /= 3.;

q = aa + bb;

r = aa - bb;

x[0] = q - a;

x[1] = (-0.5) * q - a;

x[2] = (sqrt (3.) * 0.5) * fabs (r);

if (x[2] == 0.) return (2);

return (1);

}

}

Пример 14. Алгоритм Краута (нижне-верхняя (LU) декомпозиция матрицы).

Алгоритм Краута - это фактически метод решения систем общего вида, конкурирующий по быстродействию с общеизвестным методом Гаусса-Жордана, но позволяющий более эффективно использовать решение.

Если мы можем разложить матрицу линейной системы A в произведение A=L*U, где L - нижняя, а U - верхняя треугольные матрицы, то решение системы уравнений с произвольной правой частью производится весьма просто, применением двух обратных подстановок. Более того, в отличие от известного метода Гаусса-Жордана, разложенная матрица позволяет быстро решать серии линейных уравнений с различными правыми частями при одной и той же матрице.

Метод Краута позволяет провести LU-декомпозицию матрицы примерно за то же число операций, что и "прямая" часть метода Гаусса-Жордана. Итоговые коэффициенты двух треугольных матриц упаковываются в матрицу того же размера, что и A, и на том же месте в памяти; при этом верхняя матрица U размещается в наддиагональной части и на диагонали; нижняя L в поддиагональной части, а диагональные элементы L считаются все равными 1 (без ограничения общности) и не выводятся.

Алгоритм Краута представляет из себя следующее:

1. Положить Lii=1 i=0,...,N-1 (здесь ничего не производится, а только имеется в виду для дальнейших шагов;

2. Для каждого j=0,1,...,N-1 проделать:

1. Для всех i=0,...,j вычислить Uij:

Uij=Aij - SUM(0<=k<i)(Lik*Uik) (при i=0 сумму полагать нулем);

2. Для всех i=j+1,...,N-1 вычислить:

Lij = Aij - SUM(0<=k<j)(Lik*Uik))/Uii

Обе процедуры выполняются до перехода к новому j.

Устойчивая работа алгоритма Краута возможно только при условии выбора ведущего элемента для каждого обращения к j из п.2 алгоритма. Выбор производится перед выполнением п.2 для очередного j перестановки необработанных строк матрицы так, чтобы строка, подставляемая на место j, содержала наибольший по модулю элемент в колонке j. Порядок перестановки необходимо запомнить, для чего достаточно использовать дополнительный вектор целых величин. Еще один вектор используется внутри алгоритма для масштабирования.

Алгоритм Краута имеет несколько приложений, одно из которых - решение системы линейных уравнений (обратная подстановка с учетом порядка перестановки строк), другие - вычисление обратной матрицы и вычисление детерминанта. Подробнее вызовы функций, связанных с алгоритмом Краута и реализация деталей алгоритма находятся в комментариях к программе и в ней самой.

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

#define TINY 1.e-30

/*

LU-декомпозиция в соответствии с алгоритмом Краута.

Описание:

int LU_decompos(double **a,int n,int *indx,int *d,double *vv);

Параметры:

a - исходная матрица (n x n) на входе;

n - размер матрицы;

indx - массив целых чисел (размера n) для запоминания перемещений;

d - на выходе, содержит +1 или -1 для четного или нечетного числа перемещений.

vv - временный массив (size n).

Результат:

0 - некорректная входная матрица (непригодна для декомпозиции),

1 - положительный результат.

Обратная замена, использующая LU-декомпозицию матрицы.

Описание:

void LU_backsub(double **a,int n,int *indx,double *b);

Параметры:

a - матрица, разложенная по Крауту;

n - размер матрицы;

indx - порядок перемещения, полученный алгоритмом декомпозиции;

b - вектор (размера n).

Примечание: a и indx не изменяются в этой функции и могут быть использованы повторно.

Инвертирование матрицы с использованием LU-разложенной матрицы.

Описание:

void LU_invert(double **a,int n,int *indx,double **inv,double *col);

Параметры:

a - матрица, разложенная по Крауту;

n - размер матрицы;

indx - порядок перестановки, полученный алгоритмом декомпозиции;

inv - матрица назначения;

col - временный массив (размера n).

Получение матрицы, полученной с использованием LU-разложенной матрицы

Описание:

double LU_determ(double **a,int n,int *indx,int *d);

Параметры:

a - матрица, разложенная по Крауту;

n - размер матрицы;

indx - порядок перемещения, полученный алгоритмом декомпозиции;

d - знак равенства (+1 или -1) полученный при декомпозиции.

Результат: определяющее значение.

*/

/* декомпозиция */

int LU_decompos (double **a, int n, int *indx, int *d, double *vv)

{

register int i, imax, j, k;

double big, sum, temp;

for (i = 0; i < n; i++)

{

big = 0.;

for (j = 0; j < n; j++)

if ((temp = fabs (a[i][j])) > big) big = temp;

if (big == 0.) return (0);

vv[i] = big;

}

/* главный цикл алгоритма Краута */

for (j = 0; j < n; j++)

{ /* это часть a) алгоритма, исключая i==j */

for (i = 0; i < j; i++)

{

sum = a[i][j];

for (k = 0; k < i; k++) sum -= a[i][k] * a[k][j];

a[i][j] = sum;

}

/* инициализация для поиска наибольшего элемента */

big = 0.;

imax = j;

for (i = j; i < n; i++)

{

sum = a[i][j];

for (k = 0; k < j; k++) sum -= a[i][k] * a[k][j];

a[i][j] = sum;

if ((temp = vv[i] * fabs (sum)) >= big)

{

big = temp;

imax = i;

}

}

/* обмен строк, если нужен, смена равенства и размера множителя */

if (imax != j)

{

for (k = 0; k < n; k++)

{

temp = a[imax][k];

a[imax][k] = a[j][k];

a[j][k] = temp;

}

*d =- (*d);

vv[imax] = vv[j];

}

/* сохраняем индекс */

indx[j] = imax;

if (a[j][j] == 0.) a[j][j] = TINY;

if (j < n - 1)

{

temp = 1. / a[j][j];

for (i = j+1; i < n; i++) a[i][j] *= temp;

}

}

return (1);

}

/* обратная замена */

void LU_backsub (double **a, int n, int *indx, double *b)

{

register int i, j, ip, ii = -1;

double sum;

/* первый шаг обратной замены */

for (i = 0; i < n; i++)

{

ip = indx[i];

sum = b[ip];

b[ip] = b[i];

if (ii >= 0)

for (j = ii; j < i; j++) sum -= a[i][j] * b[j];

else if (sum) ii = i; /* получен ненулевой элемент */

b[i] = sum;

}

/* второй шаг */

for (i = n - 1; i >= 0; i--)

{

sum = b[i];

for (j = i + 1; j < n; j++) sum -= a[i][j] * b[j];

b[i] = sum / a[i][i];

}

}

/* инвертирование матрицы */

void LU_invert (double **a, int n, int *indx, double **inv, double *col)

{

register int i, j;

for (j = 0; j < n; j++)

{

for (i = 0; i < n; i++) col[i] = 0.;

col[j] = 1.;

LU_backsub (a, n, indx, col);

for (i = 0; i < n; i++) inv[i][j] = col[i];

}

}

/* определяющее вычисление*/

double LU_determ (double **a, int n, int *indx, int *d)

{

register int j;

double res = (double)(*d);

for (j = 0; j < n; j++) res *= a[j][j];

return (res);

}

Пример 15. Алгоритм поиска интервала нахождения корня нелинейной функции.

Выяснение интервала, на котором корень содержится является важной проблемой поиска корня нелинейной функции действительной переменной. Здесь приведен алгоритм поиска такого интервала и ограничения на его применение. Примем, что корень функции f(x) окружен на интервале [a,b], если f(a) и f(b) имеют противоположные знаки. Чтобы окруженный согласно этому определению корень действительно существовал на этом интервале, достаточно непрерывности f(x), а для его единственности - еще и монотонности. При невыполнении этих свойств возможно отсутствие корня на [a,b] или неопределенность его позиции.

При использовании компьютера, всегда имеем дело с дискретным набором возможных представлений чисел (хотя и достаточно плотным). Кроме того, монотонность вычисленной функции может быть слегка нарушена в пределах точности ее вычисления. Это в ряде случаев усложняет вычисление окруженных корней функции, если к их точности предъявляются завышенные требования.

Окружение корня функции при гарантии ее определения на неограниченном интервале, производится по следующему итерационному алгоритму.

1. Для начального приближения x0, найти f0=f(x0), задать начальный интервал поиска D и его инкремент d>1.

2. Вычислить a=x0-D, b=x0+D; fa=f(a), fb=f(b).

3. Увеличить интервал поиска: D=D*d. Если интервал превысил некоторый заданный предел, то выйти с индикацией ошибки.

3.a. Если знаки fa и f0 отличаются, то считать корень окруженным на [a,x0] -> выход.

3.b. Если знаки fb и f0 отличаются, то считать корень окруженным на [x0,b] -> выход.

3.c. Если f0 > 0 (случай меньше нуля делается аналогично) алгоритм продолжается:

4. Проверяется, какое из fa или fb наименьшее. Если оба одинаковы, то переходим к 4a (двусторонний поиск), если fb - производим поиск вправо 4b, иначе - поиск влево 4c.

4.a. Находим a=a-D, b=b+D, fa=f(a), fb=f(b), идем к пункту 3.

4.b. Присваиваем последовательно a=x0, x0=b, fa=f0, f0=fb; находим b=b+D, fb=f(b), идем к пункту 3.

4.c. Аналогично 4b, только направление поиска - влево.

Так как интервал поиска постоянно расширяется, то в конце концов используя указанный алгоритм корень будет окружен. Возможны модификации алгоритма в двух направлениях:

1. Увеличивать интервал не в геометрической прогрессии, а в арифметической либо по заданному сценарию;

2. Если область определения функции заведомо ограничена, то расширение интервала поиска также следует ограничивать имеющимися пределами, либо доопределять функцию там, где ее оригинал не определен.

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

/*

Предполагается, что функция существует на бесконечном и непрерывном промежутке.

int BracketRoot (double x0, double *a, double *b, double d0, double di, double dmax, double (*fun)(double));

Параметры:

x0 - начальное приближение;

a - левая граница;

b - правая граница;

d0 - начальный интервал поиска;

di - интервал расширения (в геометрической прогрессии);

dmax - максимальный интервал;

fun - указатель на функцию.

Возвращает:

1 - если корень окружен;

0 - сбой

*/

int BracketRoot (double x0, double *a, double *b, double d0,

double di, double dmax, double (*fun)(double))

{

double fa, fb, f0;

f0 = (*fun)(x0);

*a = x0 - d0;

*b = x0 + d0;

fa = (*fun)(*a);

fb = (*fun)(*b);

while ((d0 *= di) < dmax)

{

if (f0 >= 0.)

{

if (fa < 0.)

{

*b = x0;

return (1);

}

if (fb < 0.)

{

*a = x0;

return (1);

}

if (fa > fb)

{

*a = x0;

x0 = (*b);

*b += d0;

fa = f0;

f0 = fb;

fb = (*fun)(*b);

}

else

if (fa < fb)

{

*b = x0;

x0 = (*a);

*a -= d0;

fb = f0;

f0 = fa;

fa = (*fun)(*a);

}

else

{

*a -= d0;

*b += d0;

fa = (*fun)(*a);

fb = (*fun)(*b);

}

}

else

if (f0 < 0.)

{

if (fa >= 0.)

{

*b = x0;

return (1);

}

else

if (fb >= 0.)

{

*a = x0;

return (1);

}

if (fa < fb)

{

*a = x0;

x0 = (*b);

*b += d0;

fa = f0;

f0 = fb;

fb = (*fun)(*b);

}

else

if (fa > fb)

{

*b = x0;

x0 = (*a);

*a -= d0;

fb = f0;

f0 = fa;

fa = (*fun)(*a);

}

else

{

*a -= d0;

*b += d0;

fa = (*fun)(*a);

fb = (*fun)(*b);

}

}

}

return (0);

}

Пример 16. Алгоритм нахождения Полинома Лагранжа (интерполяция Лагранжа).

Интерполяционный многочлеен Лагранжа - многочлен минимальной степени, принимающий данные значения в данном наборе точек. Для n + 1 пар чисел (x_0, y_0), (x_1, y_1) \ dots (x_n, y_n), где все xi различны, существует единственный многочлен L(x) степени не более n, для которого L(xi) = yi.

В простейшем случае (n = 1) - это линейный многочлен, график которого - прямая, проходящая через две заданные точки.

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

// таблица из учебника по вычислительной математике

float x[6] = {1.5, 1.54, 1.56, 1.60, 1.63, 1.70};

float y[6] = {3.873, 3.924, 3.950, 4.00, 4.037, 4.123};

/* Функция, вычисляющая коэффициенты Лагранжа

x - аргумент функции

n - степень многочлена (или число x-ов)

i - номер узла

*/

float L(float xp, int n, int i)

{

// числитель и знаменатель

float Chesl;

float Znam;

Chesl = 1;

Znam = 1;

int k;

// вычисление числителя

for (k = 0; k != n; k++ )

{

if (k == i) continue;

// убираем множитель x - x(i)

Chesl *= xp - x[k];

}

// вычисление знаменателя

for (k= 0; k!= n; k++)

{

if (x[i] == x[k]) continue;

// убираем, а то ноль в знаменателе

Znam *= x[i] - x[k];

}

return Chesl / Znam;

}

int main()

{

// вычисляем степень полинома

int n = sizeof(y) / sizeof(float);

// начальное значение

float R = 0;

// произвольная точка для проверки

float px = 1.55;

// вычисляем значение интерполяционного многочлена в точке должно получиться 3. 937075

for (int i = 0; i != n; i++)

{

R += y[i] * L(px,n,i);

}

printf("Результат : %f \n",R);

system ("pause");

}

Результат выполнения программы

Результат : 3.937075

Пример 17. Алгоритм вычисление квадратного корня по алгоритму Ньютона.

Для вычисления квадратного корня в этом примере использован метод Ньютона. Рассмотрены машинно-зависимый и машинно-независимый варианты. Перед применением алгоритма Ньютона область определения исходного числа сужается до [0.5,2] (во втором варианте до [1,16]). Второй вариант машинно-независим, но работает дольше.

Вообще, это не самый быстрый вариант, но один из самых быстрых. Основная проблема заключается в том, что нет универсального машинного представления чисел с плавающей точкой, поэтому разделение числа на мантиссу и двоичную экспоненту как составляющих его компьютерного представления нельзя записать единым образом. Имено поэтому подключено описание математической библиотеки, из которой, впрочем, используются только frexp() и ldexp(). Конкретная реализация этих функций очень проста, но машинно-зависима. При разделении числа на мантиссу и экспоненту, мантисса оказывается в пределах [0.5,1). Если при этом экспонента нечетная, мантисса умножается на 2, тем самым область определения становится [0.5,2). К мантиссе применяется алгоритм Ньютона с начальным приближением 1. Окончательный результат получается с помощью применения ldexp() с половинным значением экспоненты.

Для машинно-независимого варианта выделение экспоненты заменяется серией последовательных делений исходного значения на 16, пока аргумент не станет определяться на интервале [1,16]. Если исходный аргумент был меньше 1, то перед серией делений обращаем его. Алгоритм Ньютона применяется с начальным приближением 2. Окончательный результат получается серией последовательных умножений на 4 и дальнейшим обращением, если аргумент обращался.

Сам алгоритм Ньютона для вычисления a = Sqroot(x) представляет быстро сходящуюся (при хорошем начальном приближении) серию итераций: ai+1=0.5*(ai+x/ai), где i - номер итерации.

#include <stdio.h>

#include <stdlib.h>

#include <math.h>

#include <conio.h>

#include <string.h>

/* для одинарной точности нужны 4 итерации */

#define ITNUM 4

/*

Разложение квадратного корня аргумента в мантиссу и экспонент (быстрый алгоритм):

float Sqroot (float x);

Вычисление квадратного корня без использования внешних фукнций (медленнее, но машинно-независим):

float Sqroot1 (float x);

В случае некорректного промежутка (x<0.) фукнции возвращают 0 и не генерят ошибку.

*/

/* вариант с использованием внешних фукнций разложения/объединения на [0.5,1] */

float Sqroot (float x)

{

int expo, i;

float a, b;

if (x <= 0.F) return (0.F);

/* разложение x в мантиссу на промежутке [0.5,1) и экспонент.

Машинно-зависимые операции представлены вызовами функций. */

x = frexp (x, &expo);

/* нечетный экспонент: умножаем мантиссу на 2 и уменьшаем экспонент, делая его четным.

Теперь мантисса в промежутке [0.5,2.) */

if (expo & 1)

{

x *= 2.F;

expo--;

}

/* начальное приближение */

a = 1.F;

for (i = ITNUM; i > 0; i--)

{

b = x / a;

a += b;

a *= 0.5F;

}

/* делим экспонент на 2 и объединяем результат.

Фукнция ldexp() противоположна frexp. */

a = ldexp(a, expo / 2);

return (a);

}

/* Вариант без использования библиотек. Промежуток уменьшен до [1,16].

Используется 16 повторяющихся делений. */

float Sqroot1 (float x)

{

int sp = 0, i, inv = 0;

float a, b;

if (x <= 0.F) return (0.F);

/* аргумент меньше 1 : инвертируем его */

if (x < 1.F)

{

x = 1.F / x;

inv = 1;

}

/* последовательно делим на 16 пока аргумент не станет <16 */

while (x > 16.F)

{

sp++;

x /= 16.F;

}

/* начальное приближение */

a = 2.F;

/* Алгоритм Ньютона */

for (i = ITNUM; i > 0; i--)

{

b = x / a;

a += b;

a *= 0.5F;

}

while (sp > 0)

{

sp--;

a *= 4.F;

}

/* инвертируем результат для инвертированнго аргумента */

if (inv) a = 1.F / a;

return (a);

}

int main()

{

float x;

printf ("Введите число : ");

scanf ("%f", &x);

printf ("\nРезультат с использованием стандартных библиотек : %f\n", Sqroot(x));

printf ("Результат без использования стандартных библиотек : %f\n", Sqroot1(x));

system ("pause");

}

Результат выполнения программы

Введите число : 35

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

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

Наши рекомендации