Реализация алгоритма симплекс-метода с произвольными свободными членами

 

ФГОУ СПО Волгоградский технологический колледж

Специальность 230105 «Программное обеспечение вычислительной техники и автоматизированных систем»










КУРСОВАЯ РАБОТА

ПО ДИСЦИПЛИНЕ «Математические методы»

«Реализация алгоритма симплекс-метода с произвольными свободными членами»




Проверил:

Теткин А.А.

Выполнил:

Студент гр. ВТ-3-1

Левитин С. А






Волгоград 2011

Содержание


Введение

Постановка задачи

Математическое обеспечение

Разработка алгоритма программы

Пример работы программы

Заключение

Список используемой литературы



Введение


Основной целью данного проекта является закрепление теоретических знаний в области решения задач базовых линейного программирования симплекс - методом, получившем в литературе также название метода последовательного улучшения плана и реализация поставленной задачи на языке программирования С++.

Симплексный метод решения задач линейного программирования - вычислительная процедура, основанная на принципе последовательного улучшения решений - перехода от одной базисной точки к другой, для которой значение целевой функции больше (эти операции фиксируются в симплексной таблице). Доказано, что если оптимальное решение сушествует, то оно обязательно будет найдено через конечное число шагов (за исключением так называемой «вырожденной задачи; при которой возможно явление «зацикливания», т. е. многократного возврата к одному и тому же положению).

Данный метод был разработан американским математиком Джорджем Данцигом (George Dantzig) в 1947 году.


Постановка задачи


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


Математическое обеспечение


Примером задачи линейного программирования является целевая функция с определенным направлением экстремума и система ограничений для этой целевой функции. Например:(X) = 3x1 + 5x2 + 4x3 => max

,1x1 + 0,2x2 + 0,4x3 <= 1100

.05x1 + 0.02x2 - 0.02x3 <= 120

x1 + x2 + 2x3 <= 8000

Необходимо найти оптимальный план данной задачи с помощью симплекс-метода с использованием симплекс-таблицы.


Разработка алгоритма программы


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


Листинг 1. user_data.h

#ifndef _USER_DATA_H_

#define _USER_DATA_H_user_data {:get_data_from_user();user_data_is_valid();:*function;*fm;**system;*sign;num_v;num_l;way;

};

#endif /* _USER_DATA_H_ */


Рассмотрим все защищенные переменные-члены данного класса.num_v хранит в себе значение количества переменных исходной задачи.num_l хранит в себе значение количества ограничений исходной задачи.*function хранит значения коэффициентов целевой функции задачи. Данный член является указателем типа double, для которого в последующем будет выделена память и произведется инициализация его как одномерного массива с размером num_v.*fm хранит в себе значения свободных членов системы ограничений. Также является указателем типа double, который будет инициализирован как одномерный массив с размером num_l.**system хранит в себе значения коэффициентов самой системы ограничений. Это член является указателем на массив указателей, который в последующем будет инициализирован как матрица, соответствующая по размеру системе ограничений поставленной задачи (num_l x num_v).*sign хранит в себе знак каждого ограничения системы. Является указателем типа int, который будет инициализирован как одномерный массив типа с размером num_l. Рационально использовать в данном случае целочисленный тип а не строковый. т.к. У нас есть три знака: <=, = и >=, которые храняться в *sign как 0, 1 и 2 соответственно.way хранит в себе направление целевой функции задачи (min/max). При решении задачи на максимум эта переменная-член будет хранить в себе значение истины (true). А при решении на минимум, соответственно, ложь (false). Такой способ хранения данных очень рационален в данном случае, поскольку направлений у функции цели может быть только два. Поэтому тип bool идеально подходит для этого.

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

Листинг 2. user_data.cpp


#include <iostream>

#include <string>

#include <cstdlib>

#include "user_data.h"std::cout;std::cin;std::endl;std::string;error(int err_no)

{(err_no) {0:<< "\nВы ввели некорректное значение.\n" << endl;

break;1:<< "\nВы не можете задать менее двух ограничений.\n" << endl;;2:<< "\nВы не можете задать больше 500 ограничений.\n" << endl;;3:<< "\nВы не можете задать менее двух переменных.\n" << endl;;4:<< "\nВы не можете задать более 500 уравнений.\n" << endl;

break;

}

}user_data::get_data_from_user()

{num_limits, num_vars, s_var, fr_m, sn, func, w;i, j;validator = false;


do {<< "Введите количество ограничений в системе: ";

getline(cin, num_limits);(atoi(num_limits.c_str()) < 2)(1);if (atoi(num_limits.c_str()) > 500)(2);= true;

} while (!validator);_l = atoi(num_limits.c_str());

validator = false;{<< "Введите количество переменных в системе ограничений: ";

getline(cin, num_vars);(atoi(num_vars.c_str()) < 2) (3);if (atoi (num_vars.c_str()) > 500)(4);= true;

} while (!validator);_v = atoi(num_vars.c_str());= false;= new double [num_v];= new double *[num_l];(i = 0; i < num_l; i++)[i] = new double [num_v];= new double [num_l];= new int [num_l];

cout << "\nЗаполните коэффициенты при целевой функции.\n" << endl;

for (i = 0; i < num_v; i++) {

do {<< "Введите коэффициент целевой фукнции при x" << i + 1 << ": ";

getline(cin, func);(atof(func.c_str()) == 0)(0);{= true;[i] = atof(func.c_str());

}

} while (!validator);= false;

}{

cout << "Введите направление целевой функции ( min, max ) : ";

getline(cin, w);(w == "max" || w == "MAX" || w == "min" || w == "MIN") {= true;(w == "max" || w == "MAX")= true;= false;

}(0);

} while (!validator);<< "\nЗаполните систему ограничений.\n" << endl;(i = 0; i < num_l; i++) {

cout << "Заполните " << i + 1 << "-е ограничение.\n" << endl;

for (j = 0; j < num_v; j++) {

do {<< "Введите коэффициэнт при x" << j + 1 << ": ";

getline(cin, s_var);(atof(s_var.c_str()) == 0)(0);{= true;

}

} while (!validator);[i][j] = atof(s_var.c_str());

validator = false;

}{<< "Введите знак при " << i + 1 << "-м ограничении ( <=, =, >= ) : ";

getline(cin, sn);(sn == "<=" || sn == "=" || sn == ">=") {= true;(sn == "<=")[i] = 0;(sn == "=")[i] = 1;(sn == ">=") [i] = 2;

}(0);<< sign[i] << endl;

} while (!validator);= false;{

cout << "Введите свободный член при " << i + 1 << "-м ограничении: ";

getline(cin, fr_m);(atof(fr_m.c_str()) == 0)(0);= true;

} while (!validator);[i] = atof(fr_m.c_str());

validator = false;<< endl;

}

}


Функция error(int err_no) принимает в качестве аргумента номер ошибки, которая должна вывестись пользователю в том случае, если он ввел некорректные данные. Далее номер ошибки, переданный в функцию обрабатывается оператором switch(), и в зависимости от принимаемого функцией аргумента, выводится ошибка с помощью оператора cout.]

Теперь рассмотрим функцию-член get_data_from_user() класса user_data. Все данные, который вводит пользователь, первоначально помещаются в объект типа string, а затем проверяется корректность данных, если все введено верно, то выполняется преобразование из std::string в int или double с помощью функций atoi() и atof() соответственно.

Сначала у пользователя запрашивается количество ограничений в системе. Если было введено целое число, от 2 до 500, то это значение преобразуется в int и заностится в переменную-член num_l. В противном случае вызывается функция error() с номером соответвующей ошибки и снова запрашивается ввод данных.

Далее, таким же образом пользователь вводит количество переменных задачи.

Затем выделяется память под массив function и матрицу system, а затем также идет ввод коэффициентов функции цели в цикле. После идет ввод значения направления функции. Если оно введено верно, то в переменная-член way заносится true или false, в зависимости от введенного значения. Регистр при вводе направления не учитывается при проверке. Если все верно, заполняется матрица system коэффициентами системы ограничений исходной задачи. Заполнение происходит в двух вложенных циклах, в первом из которых, также вводится знак ограничения и значение свободного члена при этом ограничении. Когда пользователь закончит ввод, все переменная-члены класса user_data будут заполнены, и тогда мы переходим к самому алгоритму, который реализован в классе simplex, являющимся прямым наследником класса user_data. Рассмотрим содержимое заголовочного файла этого класса.

Листинг 3. simplex.h


#ifndef _SIMPLEX_H_

#define _SIMPLEX_H_

#include <sstream>

#include "user_data.h" simplex : public user_data { : init(); gen_plane(); plane_is_valid(); function_is_undefined(); print_result_to_file(int it_num); : func; **bv; **sv; *istr; *th; alm; i_lrow; i_lcol; ::stringstream table;

};

#endif /* _SIMPLEX_H_ */


Сначала рассмотрим закрытые переменная -члены данного класса.func - содержит значение целевой функции. При каждой итерации оно меняется.**bv - Содержит значения базисных переменных задачи. Данный член является указателем на массив указателей, который в последующем инициализируется двумерным массивом num_v x 2, в первом стобце которого содержатся непосредственно. Сами значения базисных переменных задачи, а во втором номера этих переменных, которые изменяются при каждой последующей итерации. Номера базисных переменных необходимы для того, чтобы правильно вывести пользователю ответ и построить таблицу на выходе.**sv - Матрица коэффициентов при переменных задачи размером num_l x num_v * 2. Первые num_v столбцы данной матрицы заполняются коэффициентами исходной системы ограничений, а последующие num_v столбцы заполняются единичной матрицей, если решается задача на максимум, если же производится решение задачи на минимум, единицы меняют свой знак. *istr - индексная строка, является одномерным массивом размером num_v * 2, первая половина которого заполняется коэффициентами функции-цели с обратным знаком, а вторая нулями на первой итерации. На последующих итерациях значения индексной строки меняются.i_lcol = индекс ведущего столбца текущего плана.*th (греч. «тета») - последний столбец симплексной таблицы, инициализируется одномерным массивом размером num_l.i_lrow = индекс ведущей строки текущего плана. alm - разрешающий элемент, находящийся на пересечении ведущего столбца и ведущей строки. ::stringstream table - объект класса std::stringstream, который содержит весь пользовательский вывод в выходной файл.

Собственно, сейчас были рассмотрены предназначения каждой переменная-члена класса simplex. Весь алгоритм вычисления вышеприведенных значений производится в файле simplex.cpp.

Листинг 4. simplex.cpp


#include <iostream>

#include <cmath>

#include <fstream>

#include <sstream>

#include <string>

#include "user_data.h"

#include "simplex.h" std::cout; std::endl; simplex::init()

{ i, j; = 0; = new double *[num_l]; (i = 0; i < num_l; i++) [i] = new double [num_v * 2]; (i = 0; i < num_l; i++) { (j = 0; j < num_v; j++) [i][j] = system[i][j]; (; j < num_v * 2; j++) (i + num_v == j) (way) [i][j] = 1; [i][j] = -1; [i][j] = 0;

} = new double [num_v * 2]; = new double *[num_l]; (i = 0; i < num_l; i++) { [i] = new double [2]; [i][0] = i + num_v; [i][1] = fm[i];

} (i = 0; i < num_v * 2; i++) (i < num_v) [i] = function[i] * -1; [i] = 0; _lcol = 0; (i = 0; i < num_v * 2 - 1; i++) { (istr[i] < 0) (fabs(istr[i + 1]) > fabs(istr[i])) _lcol = i + 1;

} = new double [num_l]; (i = 0; i < num_l; i++) [i] = bv[i][1] / sv[i][i_lcol]; _lrow = 0; (i = 0; i < num_l - 1; i++) (th[i] > th[i + 1]) _lrow = i + 1; = sv[i_lrow][i_lcol]; _result_to_file(0);

} simplex::plane_is_valid()

{ i; result = true; (way) (i = 0; i < num_v * 2; i++) (istr[i] < 0) { = false; ;

} (!way) (i = 0; i < num_v * 2; i++) (istr[i] >= 0) { = false; ;

} result;

} simplex::function_is_undefined()

{ i; (i = 0; i < num_l; i++) (th[i] < 0) { false;

} true;

} simplex::gen_plane()

{ i, j, it_num = 0; A, B; (!plane_is_valid() && function_is_undefined()) { = bv[i_lrow][1]; = istr[i_lcol]; -= A * B / alm; *tmp_bv = new double [num_l]; [i_lrow][0] = i_lcol; = bv[i_lrow][1]; (i = 0; i < num_l; i++) { = sv[i][i_lcol]; _bv[i] = bv[i_lrow][1]; (i != i_lrow) _bv[i] = bv[i][1] - A * B / alm; _bv[i] /= alm;

} (i = 0; i < num_l; i++) [i][1] = tmp_bv[i]; *tmp_istr = istr; = istr[i_lcol]; (i = 0; i < num_v * 2; i++) { = sv[i_lrow][i]; _istr[i] = istr[i] - A * B / alm;

} = tmp_istr; **tmp_sv = new double *[num_l]; (i = 0; i < num_l; i++) _sv[i] = new double [num_v * 2]; (i = 0; i < num_l; i++) (j = 0; j < num_v * 2; j++) { _sv[i][j] = sv[i][j]; = sv[i_lrow][j]; = sv[i][i_lcol]; (i == i_lrow) _sv[i][j] /= alm; _sv[i][j] = sv[i][j] - A * B / alm;

} = tmp_sv; _lcol = 0; (i = 0; i < num_l; i++) [i] = bv[i][1] / sv[i][i_lcol]; _lrow = 0; (i = 0; i < num_l -1; i++) (th[i] > th[i + 1]) _lrow = i + 1; = sv[i_lrow][i_lcol]; _num++; _result_to_file(it_num);

} (!function_is_undefined()) << "\nЦелевая фукнция не ограничена, данная задача решений не имеет\n" << endl;

else { << "\nf(x) = " << func << "\n" << endl; (i = 0; i < num_l; i++) { << "x" << bv[i][0] + 1 << " = " << bv[i][1] << endl;

} << "\nВсе вычисления были записаны в файл table.txt\n" << endl;

}

} simplex::print_result_to_file(int it_num)

{ i, j; (!it_num) {

table << "Задана целевая функция:\n" << endl;

std::stringstream f_x; _x << "f(x) = "; (i = 0; i < num_v; i++) { (!i) _x << function[i] << "x" << i + 1 << " "; { (function[i] < 0) _x << "- " << fabs(function[i]) << "x" << i + 1 << " "; (function[i] > 0) _x << "+ " << function[i] << "x" << i + 1 << " ";

}

} ::string minmax; (way) = "max"; = "min"; _x << "=> " << minmax << "\n" << endl;

table << f_x.str(); << "И система ограничений:\n" << endl;

std::stringstream math_sys; ::string math_sign; (i = 0; i < num_l; i++) { (j = 0; j < num_v; j++) { (!j) _sys << system[i][j] << "x" << j + 1 << " "; (system[i][j] == 1) (!j) _sys << "x" << j + 1 << " "; _sys << "+ x" << j + 1 << " "; (system[i][j] == -1) (!j) _sys << "-x" << j + 1 << " "; _sys << "- x" << j + 1 << " "; { (system[i][j] < 0) _sys << "- " << fabs(system[i][j])<< "x" << j + 1 << " "; _sys << "+ " << system[i][j] << "x" << i + 1 << " "; (!sign[i]) _sign = "<="; (sign[i] == 1) _sign = "="; (sign[i] == 2) _sign = ">=";

}

} _sys << math_sign << " " << fm[i]; _sys << endl;

} ::string min_or_max; (way) _or_max = "максимум"; _or_max = "минимум"; << math_sys.str() << endl;

table << "Решим данную задачу на " << min_or_max << " методом симплексных таблиц.\nПостроим первый опорный план:\n" << endl;

} (i = 0; i < num_l; i++) { << "x" << bv[i][0] + 1 << "\t" << bv[i][1] << "\t"; (j = 0; j < num_v * 2; j++) << sv[i][j] << "\t"; (!plane_is_valid()) << th[i]; << "\n" << endl;

} << "f(x)\t" << func << "\t"; (i = 0; i < num_v * 2; i++) << istr[i] << "\t"; << "\n"; (plane_is_valid()) { (plane_is_valid() && function_is_undefined())

table << "\nДанный план является оптимальным и не требует улучшения. Решение найдено." << endl; ::ofstream outfile ("table.txt"); << table.str();

} { ::string ln_or_gn; (way) _or_gn = "неположительные"; _or_gn = "положительные"; ::stringstream num_of_plane; (!it_num) _of_plane << "Первый опорный план"; _of_plane << it_num + 1 << "-й план также";

table << "\n" << num_of_plane.str() << " не является оптимальным, поскольку\nв индексной строке присутствуют " << ln_or_gn << " элементы.\nErо необходимо улучшить.\n" << endl;

}

}


Сначала выполняется инициализация первого опорного плана. Этим занимается функция-член init() класса simplex.

Значение функции-цели в первом опорном плане всегда равно нулю, поэтому в init() выполняется инициализация переменной-члена func класса simplex именно нулем.

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

После заполнения sv производится выделение памяти под одномерный массив istr и инициализация этого массива (индексной строки первого опорного плана). Первая ее часть заполняется коэффициентами целевой функции с обратным знаком, вторая ее половина инициализируется нулями.

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

Далее выделяется память под массив th и производится его инициализация. Элементы этого массива вычисляются путем деления значений базисных переменных текущего плана на соответcтвующие значения коэффициентов ведущего столбца.

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

Разрешающий элемент плана находится на пересечении ведущего столбца и ведущей строки текущего плана. После его вычисления производится вызов функции print_result_to_file(), которая заносит таблицу для первоначального опорного плана в объект table класса std::stringstream, который также является переменная-членом класса simplex.

После построения первого опорного плана необходимо вычислить оптимальный план исходной задачи. Грубо говоря, нужно производить итерирование цикла, пока план не станет оптимальным. Проверкой оптимальности плана занимается функция bool plane_is_valid, которая проверяет индексную строку текущего плана на наличие отрицательных элементов в случае решения задачи на максимум и неотрицательный в противном случае. Если таковые элементы имеются в индексной строке, то план является неоптимальным и его необходимо улучшить, поэтому функция возвращает значение false в данном случае. Если план является оптимальным, функция возвратит значение true, что будет являться сигналом о прекращении итерирования для цикла, реализованного в функции gen_pane().

Но, также, бывают случаи, когда задача не имеет решений, или, другими словами, функция цели не ограничена. Данная ситуация возникает тогда, когда в столбце th («тета») присутствуют отрицательные элементы. Данной проверкой занимается функция bool function_is_undefined(), которая возвратит истину, если в столбце th не имеется отрицательных элементов, и ложь, если таковые элементы имеются.

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

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


НЭ = СТЭ - (A * B) / РЭ.


Где «НЭ» - вычисляемые элемент нового плана, «СТЭ» - элемент предыдушего плана, соответствующий вычисляемому элементу, РЭ - разрешающий элемент предыдушего плана. Переменные A и B - это элементы старого плана, которые образуют «Прямоугольник», например.


СТЭ = 1 A = 2= 3 РЭ = 4.

В данном случае элемент нового плана будет вычисляться по вышеприведенной формуле, т. е.


НЭ = 1 - (2 * 3) / 4 = 1 - 1.5 = 0.5


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

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

Но перед тем, как вывести на экран ответ, в цикле производится вызов функции print_result_to_file(), которая в данном случает принимает в качестве аргумента номер итерации цикла, начиная с единицы. Функция пишет в объект table класса std::stringstream весь вывод, причем делает это «по умному», т. е. Формулирует весь алгоритм решения человеческим языком. Если план при текущей итерации стал оптимален, функция print_result_to_file() создает объект outfile класса std::oftream, т. е. Грубо говоря, выходной файл, в который записывается уже имеющийся объект table класса std::stringstream. Это является рациональным решением, т. к., если будет необходимо напечатать все решение на экран или еще куда-либо, нужно будет просто заменить «outfile <<» на «cout <<» или на любой другой потоковый оператор вывода.

Но, чтобы весь алгоритм, приведенный в предыдущих исходниках завелся, нам, естественно, необходима функция main(), без которой ничего работать не будет.

Листинг 5. main.cpp

#include "simplex.h" main()

{ (LC_ALL, "Russian"); *ud = new simplex; >get_data_from_user(); >init(); >gen_plane();

return 0;

}


Сначала задается русская локаль для консоли Windows, затем создается объект класса simplex, после чего вызывается функция get_data_from_user() наследуемого класса user_data, а затем init() и gen_plane() которые также были рассмотрены выше. return 0. сообщает системе об удачном завершении работы программы.

Пример работы программы.

Задана целевая функция:

(X) = 3x1 + 5x2 + 4x3 => max


И система ограничений:


,1x1 + 0,2x2 + 0,4x3 <= 1100

.05x1 + 0.02x2 - 0.02x3 <= 120

x1 + x2 + 2x3 <= 8000


Решим данную задачу с помощью программы, алгоритм которой был описан ранее.


Заглянем в файл table.txt


Решение, приведенное на данных скриншотах было проверено в MS Excel с помощью функции «Поиск решения» и является абсолютно верным, также данная таблица строилась вручную.

Исходники, тесты и сам исполняемый файл данной программы прилагаются на компакт-диске, который вложен в данную пояснительную записку.


Заключение


Основная цель данного курсового проекта - освоение теоретических знаний в области решения задач линейного программирования и получение практических навыков программирования на языке С++.

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

Данный проект несомненно, будет развиваться, в скором времени будет добавлен алгоритм решения задач методом искусственного базиса и написан графический интерфейс с использованием кроссплатформенной библиотеки QT. Усовершенствованная версия программы, возможно будет представлена в дипломной работе.


Список используемой литературы

программа симплекс таблица функция

Бьерн Страуструп - Язык программирования С++ 2-е издание 2007 год.

Лунгу К. Н. - Линейное программирование. Руководство к решению задач. - 2005 год.

Настольная книга Gentoo Linux - веб-издание, 2008 год.

А.В. Андреев - Программирования в Unix-подобных операционных системах - 2006 год.

Система управления версиями GIT, полное руководство - веб издание, 2011 год.

Также были использованы различные материалы из Википедии - Свободной веб-энциклопедии и прочих интернет-ресурсов.


ФГОУ СПО Волгоградский технологический колледж Специальность 230105 «Программное обеспечение вычислительной техники и автоматизированных систем»

Больше работ по теме:

КОНТАКТНЫЙ EMAIL: [email protected]

Скачать реферат © 2017 | Пользовательское соглашение

Скачать      Реферат

ПРОФЕССИОНАЛЬНАЯ ПОМОЩЬ СТУДЕНТАМ