Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
C# Лекция_1 Язык программирования и среда разра....docx
Скачиваний:
51
Добавлен:
18.12.2018
Размер:
1.81 Mб
Скачать
      1. Вычисление конечных и бесконечных сумм

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

(1.2)

и применить для ее решения следующий шаблон:

S=0;

for(int k=1; k<=n; k++)

{

//Вычислить текущий член суммы ak

S+=ak;

}

Часто приходится пользоваться слегка расширенным шаблоном:

Init;

for(int k=1; k<=n; k++)

{

//Вычислить текущий член суммы ak

S+=ak;

}

В этом шаблоне Init представляет группу операторов, которые инициализируют используемые в цикле переменные значения, обеспечивающие корректность применения цикла. В частном случае, рассмотренном выше, инициализация сводится к заданию значения переменной S. Заметьте, если перед началом цикла не позаботиться о том, чтобы эта переменная была равна нулю, то после завершения цикла корректность результата не гарантируется.

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

Чистка цикла. Все вычисления, не зависящие от k, следует вынести из цикла (в раздел Init).

Рекуррентная формула. Часто можно уменьшить время вычислений ak, используя предыдущее значение ak, построив рекуррентную формулу . Этот прием с успехом применяется как при вычислении функции по формуле 1.1, так и при аналогичных вычислениях большинства других математических функций.

Покажем на примере формулы 1.1, как можно построить необходимые рекуррентные соотношения. Запишем соотношения для :

(1.3)

Вычислив отношение , получим требуемое рекуррентное соотношение:

(1.4)

Значение задает базис вычислений, позволяя инициализировать начальное значение переменной ak, а соотношение 1.4 позволяет каждый раз в теле цикла вычислять новое значение этой переменной. Заметьте: введение рекуррентного соотношения позволило избавиться от вычисления факториалов и возведения в степень на каждом шаге цикла.

Иногда следует ввести несколько дополнительных переменных, хранящих вычисленные значения предыдущих членов суммы. Рекуррентная формула выражает новое значение ak через предыдущее значение и дополнительные переменные, если они требуются. Начальные значения ak и дополнительных переменных должны быть корректно установлены перед выполнением цикла в разделе Init. Заметьте, если начальное значение ak вычисляется в разделе Init до цикла, то схема слегка модифицируется - вначале выполняется прибавление ak к S, а затем новое значение ak вычисляется по рекуррентной формуле.

А теперь поговорим о том, как справляться с бесконечными суммами, примером которых является формула 1.1. Для математики бесконечность естественна. Множество целых чисел бесконечно, множество рациональных чисел бесконечно, множество вещественных чисел бесконечно. Элементы первых двух множеств можно пронумеровать - они задаются счетными множествами, множество вещественных чисел несчетно. Сколь угодно малый промежуток вещественной оси мы бы не взяли, там находится бесконечно много вещественных чисел. Число и другие иррациональные числа задаются бесконечным числом цифр, не имеющим периода.

Мир компьютеров - это конечный мир, хотя в нем и присутствует стремление к бесконечности. Множества, с которыми приходится оперировать в мире компьютера, всегда конечны. Тип целых чисел в языках программирования - int - всегда задает конечное множество целых из некоторого фиксированного диапазона. В библиотеке FCL это наглядно подтверждается самими именами целочисленных типов System.Int16, System.Int32, System.Int64. Типы вещественных чисел - double, float - задают конечные множества. Это достигается не только тем, что диапазон задания вещественных чисел ограничен, но и ограничением числа значащих цифр, задающих вещественное число. Поэтому для вещественных чисел компьютера всегда можно указать наборы таких двух чисел, между которыми нет никаких других чисел. Иррациональности компьютер не знает - число ? всегда задается конечным числом цифр.

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

По определению число является пределом числовой последовательности, если для любого сколь угодно малого числа существует такой номер , зависящий от , что для всех , больших , числа находятся в -окрестности числа . Это определение дает основу для вычисления значения предела . Понятно, что получить точное значение во многих случаях принципиально невозможно, - его можно вычислить лишь с некоторой точностью и тоже не сколь угодно малой, поскольку существует понятие "машинного нуля" - минимального числа, все значения меньше которого воспринимаются как нуль. Когда два соседних члена последовательности - и - начинают отличаться на величину по модулю меньшую чем , то можно полагать, что оба члена последовательности попали в -окрестность числа и можно принять за приближенное значение числа . Это рассуждение верно только при условии, что последовательность действительно имеет предел. В противном случае этот прием может привести к ошибочным выводам. Например, рассмотрим последовательность, элементы которой равны 1, если индекс элемента делится на 3, и равны 2, если индекс не делится на 3. Очевидно, что у этой последовательности предела нет, хотя существуют полностью совпадающие соседние члены последовательности.

При вычислении на компьютере значения функции, заданной разложением в бесконечный сходящийся ряд, не ставится задача получения абсолютно точного результата. Достаточно вычислить значение функции с заданной точностью . На практике вычисления продолжаются до тех пор, пока текущий член суммы не станет по модулю меньше заданного . Чтобы этот прием корректно работал, необходима сходимость ряда.

Вернемся к задаче вычисления функции . Вот возможный шаблон решения:

Init;

while(Abs(ak) > EPS)

{

S+=ak;

k++;

//Вычислить новое значение ak

}

При применении этого шаблона предполагается, что в разделе Init объявляются и должным образом инициализируются нужные переменные - S, ak, k. По завершению цикла переменная S содержит значение функции, вычисленное с заданной точностью.

Теперь мы готовы расширить определение класса, добавив код метода.

      1. Код

Приведем полный код проекта DLL, построенный на данный момент:

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

namespace MathTools

{

/// <summary>

/// Аналог класса Math библиотеки FCL

/// </summary>

public class MyMath

{

//Константы класса

const double TWOPI = 2 * Math.PI;

const double EPS = 1E-9;

//Статические методы класса

/// <summary>

/// Sin(x)

/// </summary>

/// <param name="x">

/// угол в радианах - аргумент функции Sin

/// </param>

/// <returns>

/// Возвращает значение функции Sin для заданного угла

/// </returns>

public static double Sin(double x)

{

//Оптимизация - приведение к интервалу

x = x % TWOPI;

//Init

double a = x;

double res = 0;

int k = 0;

//Основные вычисления

while (Math.Abs(a) > EPS)

{

res += a;

a *= -x * x / ((2 * k + 2) * (2 * k + 3));

k++;

}

return res;

}

}

}

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

Итак, все хорошо? Не совсем. Оптимизацию можно продолжить, правда, не столь уже существенную. Сейчас для вычисления значения переменной a требуется выполнить одно деление, пять умножений, два сложения, взятие результата с обратным знаком. Попробуйте самостоятельно написать новую версию метода с улучшенными показателями, не глядя на код, который я сейчас приведу. Я добавил в класс новую версию метода, сохранив для новой версии имя метода - Sin. В классе остался и старый метод, но уже с именем SinOld. Две версии, давая один и тот же результат вычислений, позволят нам в дальнейшем провести некоторые полезные исследования.

Вот код метода с дополнительной оптимизацией:

public static double Sin(double x)

{

//Оптимизация - приведение к интервалу

x = x % TWOPI;

//Init

double a = x;

double res = 0;

int k = 0;

double x2 = x * x;

//Основные вычисления

while (Math.Abs(a) > EPS)

{

res += a;

k+=2;

a *= -x2 / (k * (k + 1));

}

return res;

}

Код метода стал элегантнее и короче: вместо пяти умножений теперь делается только два, и вместо двух сложений - одно.

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

А теперь вернемся к технической стороне дела. Построим Решение, содержащее проект, для чего в Главном меню среды выберем пункт Build|Build Solution. В результате успешной компиляции будет построен файл с уточнением dll. Поскольку построенная сборка не содержит выполняемого файла, то непосредственно запустить наш проект на выполнение не удастся. Построим консольный проект, к которому присоединим нашу DLL, и протестируем, насколько корректно работают созданные нами методы. Заодно разберемся с тем, как строится консольный проект и как к нему подсоединяется сборка, содержащая DLL.