Математика. Настоящий учебник посвящен системе Mathematica прикладному пакету компьютерной алгебры, при помощи которого можно решать любые задачи, в которых в той или иной форме встречается математика
Скачать 4.43 Mb.
|
— ну в самом деле, не спрашивать же Timing[x]; — кстати, почему? • Время показываемое Timing[x] отвечает приращению TimeUsed[] по- сле вычисления x. Это можно сформулировать и в обратную сторону: TimeUsed[] суммирует Timing[x] для всех вычисленных на протяжении сессии выражений x. • Команда Timing[x] контролирует только время работы CPU необходи- мое собственно для вычисления x. Время, затраченное на форматирование и вывод результата, общение с другими программами и пр. не учитыва- ется. Если Вы хотите измерить все время необходимое для вычисления и представления результата, используйте AbsoluteTiming[x]. • К порождаемым командами Timing и AbsoluteTiming результатам не следует относиться слишком серьезно. Конечно, разницу между 0.002 се- кундами, 2 секундами и 2000 секундами в большинстве случаев трудно игнорировать. В то же время разница между 2 и 3 секундами обычно не является показательной. Дело в том, что это время зависит не только от качества алгоритма, но и от используемой компьютерной системы, объ- ема доступной памяти, особенностей операционной системы, эффективно- сти компиляции используемого фрагмента кода, других процессов, которые одновременно исполняются на том же компьютере, и — не в последнюю очередь!!! — от того, в какой момент сессии это вычисление производится. • Обычно необходимое для вычисления время может быть определено лишь с точностью до некоторой константы, описывающей степень само- контроля компьютера, на котором производится вычисление. Наименьшее время, которое может быть эффективно замерено, называется $TimeUnit. Для большинства бытовых компьютеров, работающих под Windows, это вре- мя равно 1 миллисекунде, т.е. 1/1000 секунды. Таким образом, в этом слу- чае относительно вычисления, про которое команда Timing заявляет, что оно требует 0 секунд работы CPU, можно с уверенностью утверждать лишь то, что оно занимает меньше 1 миллисекунды. Многие машины, рабо- тающие под UNIX, пользуются более мелким зерном. Команда TimeConstrained[x,t] пытается вычислить x за t секунд и пре- рывает вычисление, если расчет не укладывается в отведенное время. Яс- но, что в диалоговом режиме эта команда практически бесполезна, так как мы и так можем прервать вычисление в любое время. Эта команда стано- вится однако вполне осмысленной в составе длинной программы, в которой мы предлагаем попробовать несколько различных подходов к вычислению одного и того же выражения. 193 § 6. Модификация значения переменной В отличие от математиков, программисты вопринимают переменную действительно как переменную, которая в разные моменты времени при- нимает разные значения. Команды n = n + 1 и n = n − 1 встречаются в традиционном программировании настолько часто, что им удобно присво- ить специальные названия и обозначения. n++ Increment[n] увеличить n на 1 после вычисления n-- Decrement[n] уменьшить n на 1 после вычисления ++n PreIncrement[n] увеличить n на 1 перед вычислением --n PreDecrement[n] уменьшить n на 1 перед вычислением Различие между Increment[n] и Decrement[n] совершенно понятно. По- сле выполнения первой из этих команд значение n увеличивается на 1, а после выполнения второй — уменьшается на 1. Различие между Increment и PreIncrement гораздо тоньше, по крайней мере с точки зрения того, кто никогда не практиковался в процедурном программировании. В отличие от предыдущей эта оппозиция не является математической, а целиком ле- жит в области flow of calculation. Ее можно объяснить так. Все эти команды берут старое значение переменной n и перерабатывают его в новое значение, равное n + 1 или n − 1. При этом ++ как в постпозиции, так и в препозиции увеличивает его на 1, так что как n=1; n++; n, так и n=1; ++n; n дадут 2 — проверьте это!!! Так в чем же тогда разница между ними? Дело в том, что мы спросили про значение n слишком поздно, чтобы заметить эту разницу!! До выполнения ++ используется старое значение, после выполнения — новое. Существует ровно один момент, обращение к n во время инкрементации, который покажет эту разницу. А именно, в этот момент n++ использует старое значение, в то время как ++n — уже новое. Тем самым, n=1; n++ даст 1, в то время как n=1; ++n даст 2. Подробнее за работой этих команд можно проследить по следующему диалогу, в ко- тором мы предлагаем выдать 10 инкрементаций или декрементаций (вызов итератора в формате {10} без имени итератора интерпретируется просто как количество повторений), после чего смотрим на получившееся значе- ние i. Мы видим, что 10 инкрементаций увеличивают значение на 10, а 10 декрементаций уменьшают его на 10, но вот текущие значения в процессе выполнения этих процедур различны: In[7]:= {i=1; Table[i++,{10}], i} Out[7]= {{1,2,3,4,5,6,7,8,9,10},11} In[8]:= {i=1; Table[++i,{10}], i} Out[8]= {{2,3,4,5,6,7,8,9,10,11},11} In[9]:= {i=1; Table[i--,{10}], i} Out[9]= {{1,0,-1,-2,-3,-4,-5,-6,-7,-8},-9} In[10]:= {i=1; Table[--i,{10}], i} 194 Out[10]= {{0,-1,-2,-3,-4,-5,-6,-7,-8,-9},-9} Обратите внимание на формат этих команд. Мы вызываем список из минипрограммы i=1; Table[i++, {10}] и i. Почему мы не могли спросить просто i=1; Table[i++, {10}]; i, без всякого списка? Потому что в этом случае мы увидели бы только окончательные значения i после выполнения вычисления, а не сам ход вычислений. Еще раз обратите внимание на тот факт, что на уровне окончательных значений никакой разницы между i++ и ++i нет!!! Примерно так же как инкремент и декремент используются и другие команды модификации значений переменной: x+=d AddTo[x,d] прибавить d к x x-=d SubtractFrom[x,y] вычесть d из x x*=c TimesBy[x,c] умножить x на c x/=c DivideBy[x,c] разделить x на c Таким образом, x+=d является просто сокращением для x=x+d, а x-=d — сокращением для x=x-d. Точно так же x*=с является сокращением для x=x*с, а x/=d — сокращением для x=x/с. Эти сокращения бывают удобны в тех случаях, когда переменная x длинное имя, которое мы не хотим пе- репечатывать. Во всех остальных случаях мы предпочитаем пользоваться явными присваиваниями x=x+d, x=x-d, x=x*с и x=x/с. Между модификаци- ей значения переменной и присваиванием имеется существенное различие: • Чтобы значение переменной можно было модифицировать, она должна уже иметь значение. Попытка выполнить что-нибудь в духе x+=y не приведет к успеху, если x до этого не присваивалось зна- чения. Результатом такой попытки будет сообщение AddTo: a is not a variable with a value, so its value cannot be changed. С другой стороны, x=y; x+=z даст y+z. То же самое относится, конечно, и ко всем остальным операциям модификации значений, в том числе к инкременту и декременту. Вот, например, программа для вычисления 1 m + . . . + n m , выполненная с помощью команды +=: summa[m ,n ]:=Block[ {i,sum=0}, Do[sum+=i^m,{i,1,n}]; Return[sum]] Использование остальных идиом нам уже известно. Однако в Махабхарате утверждается, что Брахма так выражался по этому поводу: повтореба мать учебы, глупец не поймет и после 10000 повторений, а муд- рому достаточно всего 2500 повторений. Поэтому давайте еще раз резюмируем все лингвистические моменты: ◦ Бланк показывает, что переменные m и n здесь являются пустыш- ками (dummy variables = фиктивные или немые переменные), которые можно заменить на любое другое выражение. 195 ◦ Отложенное присваивание := показывает, что правая часть является не окончательным значением, которое мы хотим присвоить левой части, а программой для вычисления этого значения. Эта программа будет запу- щена в тот момент, когда мы вместо пустышек подставим в левую часть настоящие численные значения. ◦ Блок Block служит для локализации значений переменных i,sum. В действительности локализация i излишня, так как Do сама локализует свой итератор — но мы можем этого не знать!! С другой стороны, локализация sum тоже не обязательна, если мы не будем называть наших переменных sum— а мы этого делать не будем, так как никогда не используем в глобаль- ном контексте имен, лишь в одной позиции отличающихся от внутренних имен. Но локализация промежуточных переменных/значений за- ведомо не может ничему помешать, а вот отсутствие локализации там, где она необходима, может привести к серьезным проблемам. Де- виз серьезного пользователя: sicher ist sicher, sicuro ´e sicuro, better safe, than sorry. ◦ Block вызывается с двумя аргументами, списком локальных перемен- ных и собственно программой, отдельные команды этой программы разде- ляются точкой с запятой. Использование запятой вместо точки с запятой для разделения команд является грубой синтаксической ошибкой. ◦ Команда Do имеет два аргумента, вначале один, описывающий упраж- нение, которое эта команда намеревается проделывать, а потом второй, который задает имя итератора, с которым команда собирается это проде- лать, его начальное и конечное значение. Как всегда, по умолчанию шаг итерации равен 1. Кроме того, по умолчанию и начальное значение итера- тора равно 1, так что в принципе мы могли бы задать второй аргумент в виде {i,n}, но мы считаем, что это bad form (= дурной стиль). ◦ Команда Return[sum] служит для возвращения текущего значения sum из блока на глобальный уровень. Упражнение. Напишите программу, вычисляющую сумму m-х степеней первых n нечетных натуральных чисел. § 7. Немедленная и отложенная подстановка: -> Rule versus :> RuleDelayed Следует отдавать себе отчет, что присваивание x = 3 имеет глобальный характер, и сохраняет силу на протяжении всей сессии, до тех пор, пока x не было присвоено новое значение или пока значение x не было модифи- цировано. Поэтому если Вы хотите просто посмотреть, чему равно значе- ние f (x) какой-то функции при x = 3, значительно удобнее пользоваться не присваиванием, а другой конструкцией, подстановкой. Подстановка оформляется посредством Rule и Replace или их вариантов. В простей- шем виде для вычисления значения f (3) можно ввести, например, текст, f[x] /. x->3, который с тем же успехом, что x=3; f[x] вычисляет f (3), 196 но при этом обладает тем неоспоримым преимуществом, что не присваивает x никакого перманентного значения. Хороший стиль программирования. Старайтесь вообще избегать явно- го присваивания значений тем переменным, которые не имеют уникальных имен. Вместо этого пользуйтесь подстановками. x->y Rule[x,y] немедленная подстановка x:>y RuleDelayed[x,y] отложенная подстановка Различие между Rule и RuleDelayed в смысле flow of calculation в точ- ноcти такое же, как между Set и SetDelayed. А именно, • Когда мы выполняем непосредственную подстановку lhs->rhs, ее пра- вая часть rhs вычисляется один раз в тот момент, когда мы задаем подстановку; • Когда мы выполняем отложенную подстановку lhs:>rhs, ее правая часть rhs заново вычисляется каждый раз, когда мы обращаемся к значению ее левой части lhs. Приведем пример наглядно демонстрирующий драматическое разли- чие между Rule и RuleDelayed с точки зрения flow of calculation. Опре- делим список xxx посредством Table[x, {10}]. Тогда xxx=== {x,x,x,x,x,x,x,x,x,x}. Посмотрим на два следующих диалога: In[11]:=n=1; xxx /. x->n++ Out[11]= {1,1,1,1,1,1,1,1,1,1} In[12]:=n=1; xxx /. x:>n++ Out[12]= {1,2,3,4,5,6,7,8,9,10} В нашем инпуте использована команда инкремент n++ Increment, которая увеличивает значение n на 1 после использования текущего значения n в вычислении. Мы видим, что при применении Rule значение n вычисляется один раз, в тот момент, когда задано правило x->n++. С другой сто- роны при применении RuleDelayed значение n вычисляется каждый раз заново при каждом обращении к x. Упражнение. Ответьте, не включая компьютер, что получится, если за- менить в этих программах инкремент на преинкремент ++n который увели- чивает значение n на 1 перед использованием текущего значения n; а так- же на декремент n-- или предекремент --n, которые уменьшают значение n на 1 после использования или, соответственно, перед использованием текущего значения n в вычислении. • Правила lhs->rhs и lhs:>rhs применяются при помощи описанной в следующем параграфе команды Replace и ее усиленных вариантов /. ReplaceAll, //. ReplaceRepeated и т.д. 197 • Описанное в § 4 немедленное присваивание lhs=rhs трактуется систе- мой как приказ на протяжении всей сессии применять немедленную подста- новку lhs->rhs всюду, где ее можно применить, в то время как варианты команды Replace применяет ее только в явно указанных местах. • Точно так же описанное в предыдущем параграфе отложенное при- сваивание lhs:=rhs трактуется системой как приказ на протяжении всей сессии применять отложенную подстановку lhs:>rhs всюду, где ее можно применить, в то время как команда Replace применяет ее только в явно указанных местах. • Модификация опций большинства встроенных функций осуществляет- ся путем включения подстановки Option->Choice в тело функции. Чтобы изменить значения нескольких опций, нужно написать Option1->Choice1, Option2->Choice2, ... Вот типичнейший пример изменения опции посредством команды Rule. По умолчанию команда Factor ищет разложение многочлена f над Z, но задав опцию Extension-> {a,b,c} можно предложить ей раскладывать многочлен над Z[a, b, c]: ◦ Factor[x^4+1] предлагает разложить этот многочлен над Z, но над Z он неприводим, так что мы снова увидим 1+x^4; ◦ Factor[x^4+1,Extension->{I}] предлагает разложить тот же много- член над кольцом Z[i] целых гауссовых чисел. В этом случае мы получим ответ (-I+x^2)(I+x^2); ◦ Factor[x^4+1,Extension->{Sqrt[2]}] предлагает разложить тот же многочлен над кольцом Z[ √ 2]. В этом случае мы получим совершенно другое разложение, -(-1+ √ 2-x^2)(1+ √ 2+x^2). • Многие команды выдают ответ в формате x->c. Например, вычисляя при помощи Solve[a*x+b==0,x] решение линейного уравнения ax + b = 0 мы получим ответ в виде {{x->-b/a}}. • В выражении можно одновременно произвести несколько подстановок. Самый удобный формат для этого состоит в том, чтобы задавать подста- новки как список . Это можно сделать заранее. Например, вычисление In[13]:=rules= {x->a,y->b}; f[x+y] /. rules даст нам f[a+b] но при этом, как всегда, сами x и y не получают никаких постоянных значений. С другой стороны, список подстановок сохранится и чтобы вычислить g[a,b] нам достаточно будет напечатать g[x,y] /. rules. • В подстановках и отложенных подстановках можно использовать блан- ки и паттерны для обозначения того, что подстановка должна осуществ- ляться для произвольных объектов из некоторого домена. Подстановка, применямая не к индивидуальным объектам, а к паттернам — которые мо- гут в свою очередь заменяться на произвольные объекты!!! — называется правилом преобразования (transformation rule). 198 Приведем игрушечный пример применения правил преобразования. На- пример, Mathematica автоматически не пользуется формулой для танген- са суммы для вычисления чего-нибудь наподобие Tan[a+Pi/3]. Приме- нив к этой функции TrigExpand с последующим Simplify, мы получим что-нибудь в духе ( √ 3*Cos[a]+Sin[a])/(Cos[a]- √ 3*Sin[a]). Но мы хо- тим явно увидеть, что получается в результате применения к Tan[a+Pi/3] именно формулы для тангенса суммы. Это позволяет сделать следующая конструкция: In[14]:=Hold[Tan[a+Pi/3]] /. Tan[x +y ]->(Tan[x]+Tan[y])/(1-Tan[x]*Tan[y]) Функция Hold здесь использована, чтобы удержать Mathematica от неко- торых лишних в данном случае преобразований. Последующее применение ReleaseHold[%] даст желаемое представление: Out[14]=( √ 3+Tan[a])/(1- √ 3*Tan[a]) Обратите внимание на синтаксис lhs[x ,y ]->rhs[x,y]: • левая часть правила преобразования имеет формат lhs[x ,y ], бланки при переменных указывают, что это пустышки, называемые также немы- ми или фиктивными переменные (dummy variables), вместо которых можно подставить что угодно; • правая часть правила преобразования имеет формат rhs[x,y], но при этом бланки при немых переменных не ставятся!!! • Если бы мы хотели применять это правило и в дальнейшем, мы бы, скорее всего, задали его как отложенное правило преобразования, в формате lhs[x ,y ]:>rhs[x,y]. • Как мы узнаем в Главе 5, применение отложенных правил преобра- зования имеет то преимущество, что они могут принимать условия, вве- денные посредством оператора /; Condition. Языковая тонкость здесь состоит в том, что если мы описываем правило преобразования само по себе, оно имеет формат lhs[x ,y ] :>rhs[x,y] /; conditions Однако если мы хотим применить это правило при помощи оператора заме- ны /. ReplaceAll, то условный оператор вносится в левую часть правила преобразования: (lhs[x ,y ] /; conditions):>rhs[x,y] Mathematica автоматически не упрощает таких выражений, как ln(eπ). До- пустим, мы хотим провести вычисление в духе школьной математики, ис- пользующее формулу ln(xy) = ln(x) + ln(y) только в том случае, когда x, y > 0. Для этого правило нужно задавать в виде In[15]:=Log[E*Pi]/.(Log[x *y ] /; N[x]>0&&N[y]>0):>Log[x]+Log[y] 199 даст 1+Log[Pi]. То, что мы сейчас увидели, представляет собой первую крошечную де- монстрацию тех огромных возможностей, которые появятся у читателя в тот момент, когда он овладеет функциональным программировани- ем, которое позволит ему определять функции и правила преобразования для них, а также управлять применением функций к разным фрагментам выражений и правил преобразования на разных этапах вычисления. |