⬆️ ⬇️

Discrete Mathematics for Freshmen: Teacher Experience

Today I have an unusual text that is completely unrelated to machine learning (for new readers: this text is part of the blog of Surfingbird , in which I talked about different machine learning devices in the last few years in the annex to recommender systems). In this post there will be almost no mathematical part, but there will be a description of a very simple program that I wrote for my students. It is unlikely that someone will learn a lot of new content from this post, but it seems to me that the idea itself is of some value - many people simply don’t think that "it’s possible anyway." So…







Formulation of the problem



')

In this semester, I began a somewhat unusual activity: I teach discrete mathematics for freshmen in the St. Petersburg branch of the Higher School of Economics; I have been teaching for a long time, but it seems that I have never had students under the fourth or fifth year before. Under the link you can find a summary of the course, and even that is incomplete (the course is still under way), but this is not really about that.



I think most of the inhabitants of Habra even approximately remember what “discrete mathematics is for freshmen”: propositional logic, conjunctive and disjunctive normal forms, bases of Boolean functions, binary relations, partial orders ... In general, nothing is conceptually complex, but these are all things which must be mastered very firmly, any mathematical education rests on them, and the need to consciously think about them will quickly become an unaffordable luxury. Therefore, you need a lot of practical examples and tasks to "get a hand".



As one of the forms of reporting, I chose the “big homework”: several practical examples that need to be solved. This form has many advantages: students work at their own pace, I can also check how much you need, everything happens in writing, which is always convenient. But there are also disadvantages; the main disadvantage is simple - it is difficult to make and test fifty options for fifty students (there are about as many on the stream). And if you give one option to a large group, it is clear that you will get beautifully rewritten correct answers at the output, especially considering that in such basic discrete mathematics the “solution course” is often simply absent (how to build the SDNF — well, look at the truth table and write ...).



To get around this problem, I decided to write a simple program that will generate an individual homework for each student at random. The idea is extremely simple, but for some reason I never met her when I was a student, or in a later teaching experience - actually, that's why I am writing this post. I will give a minimal working example, and then I will show what I got as a result.



Templates in .tex and boost :: format





The basic technology is clear - you need to make a LaTeX-blank into which to insert specific tasks for each student. It doesn’t matter what language to do it, I am historically accustomed to writing small programs in C ++, so I’ll use it here. The easiest way to do this in C ++, which I know, is boost :: format : it is enough to make blanks with placeholders like% 1%,% 2%, and then you can insert anything there (boost :: format has other possibilities , but we do not need them now).



So, we make templates. First, the general template of the "abstract LaTeX document":



Little TeX
\documentclass[a4paper]{article} \usepackage[utf8]{inputenc} \usepackage{amsthm,amsmath,amsfonts, amssymb} \usepackage[english,russian]{babel} \usepackage{concrete} \usepackage{enumerate} \usepackage{euler} \usepackage{fullpage} \pagestyle{empty} \selectlanguage{russian} \begin{document} \selectlanguage{russian} %1% \end{document} 




Then a specific template of the actual task - we will substitute it instead of% 1%. I here, as promised, give a minimal example. We will generate only one task: according to a given Boolean formula, translate it into several other forms.



Little TeX
   \hfill %1%  $2013$ \hfill  %2% \section*{ } \begin{enumerate} \item     $$ \varphi = %3%: $$ \begin{enumerate}[(i)] \item   ; \item  $\varphi$        ; \item  $\varphi$    ; \item $[\ast]$  $\varphi$     $x\mid y = \lnot(x\land y)$. \end{enumerate} \pagebreak 




And now we just need to generate, than fill in% 3% (instead of% 1% and% 2% the student’s name and group number will be substituted). For this you need to learn how to generate formulas. Immediately I warn you that I am a bad programmer, and the code below probably resembles spaghetti - in principle, it works, if someone advises an elegant refactoring, I will say thank you.



First you need to have a structure that will store different bundles and types of nodes in the formula; for our minimal example, this is unnecessary, but I had to do another task about formulas for algebra of sets (union, intersection and symmetric differences), and I wanted to reuse the code about formula trees. Therefore, here is a global object that stores basic information:



C ++ code
 class Globals { public: size_t TypesNo; size_t VarType; vector<string> TypesLatex; vector<size_t> TypesArity; boost::random::uniform_int_distribution<> RandomNodeType; boost::random::uniform_int_distribution<> RandomNodeNoVar; boost::random::uniform_int_distribution<> RandomNodeBinary; size_t VarsNo; vector<string> VarNames; boost::random::uniform_int_distribution<> RandomVarType; size_t current_varnum; Globals(size_t types_num, vector<string> types_latex, vector<size_t> types_arity, size_t var_num, vector<string> var_names) : TypesNo(types_num), VarType(types_num - 1), TypesLatex(types_latex), TypesArity(types_arity), RandomNodeType(0, types_num - 1), RandomNodeNoVar(0, types_num - 2), RandomNodeBinary(0, types_num - 3), VarsNo(var_num), VarNames(var_names), RandomVarType(0, var_num - 1), current_varnum(var_num - 1) {} size_t get_next_var() { current_varnum++; if (current_varnum == VarsNo) current_varnum = 0; return current_varnum; } }; Globals GBoolean(7, { "\\land", "\\lor", "\\rightarrow", "\\oplus", "\\equiv", "\\lnot", "\\var" }, { 2, 2, 2, 2, 2, 1, 0 }, 4, { "x", "y", "z", "t" }); 




Here we created the “language of Boolean formulas”, which has five binary bundles (conjunction, disjunction, implication, XOR and equivalence) and one unary (negation). The seventh type of node is a variable, it has an arity of 0. In my homework, I limited myself to the formulas of four variables: less is not enough, and more is becoming too cumbersome. It is also convenient to write here the generators of a random type of a node, a random variable, a random binary connection (I used the distributions from boost :: random - again, very conveniently, although there is not much that is implemented, but we don’t need much now).



This structure will be easily reused for set algebra formulas (this is just for comparison, no further GSet will be used):



C ++ code
 Globals GSet(5, { "\\cap", "\\cup", "\\triangle", "\\overline", "\\var" }, { 2, 2, 2, 1, 0 }, 3, { "A", "B", "C" }); 




Now create a formula class. A Boolean formula is a tree whose leaves are variables, and the internal vertices are logical connectives. We want to be able to generate formulas of a given depth, so we will pass to the constructor if it’s time to make this node a leaf or, on the contrary, necessarily a binary bundle. If you need to create a random node, we will pass the type g-> TypesNo. If the node turns out to be a sheet, it needs to generate a variable (so that the variables are likely to fall into everything, we just take them in a circle - formulas, of course, are not completely random, but this is not terrible).



C ++ code
 class BNode { public: Globals *glob; size_t type; size_t varnum; BNode * left; BNode * right; BNode(Globals *g, size_t t, bool must_be_leaf = false, bool must_not_be_leaf = false, bool must_be_binary = false) : glob(g), type(t), left(NULL), right(NULL) { if (t == g->TypesNo) { // this means we want a random node type = must_be_leaf ? g->VarType : (must_be_binary ? g->RandomNodeBinary(gen) : (must_not_be_leaf ? g->RandomNodeNoVar(gen) : g->RandomNodeType(gen) )); } varnum = (type == g->VarType) ? g->get_next_var() : 0; } ~BNode() { if (left != NULL) delete left; if (right != NULL) delete right; } }; 




Now we begin to fill the class BNode. The main thing for us is that the formula is successfully printed in LaTeX:



C ++ code
  string TypeString() const { if (type == glob->VarType) return glob->VarNames[varnum]; return glob->TypesLatex[type]; } string ToString() const { if (glob->TypesArity[type] == 0) return TypeString(); if (glob->TypesArity[type] == 1) return TypeString() + "{" + left->ToString() + "}"; return "(" + left->ToString() + " " + TypeString() + " " + right->ToString() + ")"; } 




In addition, you will need to be able to calculate the value of the formula on a given set of variables:



C ++ code
  bool get_truth_value(const vector<bool> & vals) { switch(type) { case 0: return left->get_truth_value(vals) && right->get_truth_value(vals); break; case 1: return left->get_truth_value(vals) || right->get_truth_value(vals); break; case 2: return (!left->get_truth_value(vals)) || right->get_truth_value(vals); break; case 3: return left->get_truth_value(vals) != right->get_truth_value(vals); break; case 4: return left->get_truth_value(vals) == right->get_truth_value(vals); break; case 5: return !left->get_truth_value(vals); break; case 6: return vals[varnum]; break; default: return false; break; } } 




Let's leave BNode class for now (we'll return to it); now we can write a random formula generator. We will generate a formula with a specified minimum and maximum depth (to maintain the minimum depth, we added the must_not_be_leaf field to the constructor earlier):



C ++ code
 BNode *generate_tree(Globals & g, size_t min_depth, size_t max_depth, bool must_be_binary = false) { if (max_depth == 0) return NULL; BNode *node = new BNode(&g, g.TypesNo, max_depth == 1, min_depth > 0, must_be_binary); if (g.TypesArity[node->type] == 1) { node->left = generate_tree(g, min_depth, max_depth, true); } if (g.TypesArity[node->type] == 2) { node->left = generate_tree(g, min_depth - 1, max_depth - 1); node->right = generate_tree(g, min_depth - 1, max_depth - 1); } return node; } 




Everything is self-evident; the only solution I took here was to make the unary functions (ie, negations) “free”, not considered for depth, otherwise the formulas would turn out to be too simple. In addition, in the Boolean formula it is logical to prohibit putting two negatives in a row, it is meaningless; for this we needed the must_be_binary flag in the constructor.



And you can write a file handler with a list of students:



C ++ code
 void process_one_student_file_boolean(string dir, string fname, boost::format & general_tmpl, boost::format & problem_tmpl, boost::format & solution_tmpl) { BNode *node_bool; ostringstream s; vector<string> students = readAllLinesFromFile(dir + "/" + fname + ".txt"); cout << "\t " << fname << endl; for (size_t i=0; i<students.size(); ++i) { if (students[i].size() == 0) continue; // empty line cout << "\t\t[ " << students[i] << " ]" << endl; node_bool = generate_tree(GBoolean, 2, 4); string group_string = "$" + fname + "$"; s << problem_tmpl % students[i] % group_string % node_bool->ToString(); delete node_bool; } ofstream ofs(dir + "/" + fname + ".tex"); ofs << general_tmpl % s.str() << endl; ofs.close(); } 




and then main, which reads files with formats and processes files with student lists:



C ++ code
 string students_dir = "2013"; vector<string> students_files = { "BoTR" }; int main(int argc, char *argv[]) { boost::format boolean_tpml( read_file_as_string("boolean_problem_minimal.tex") ); boost::format general_tmpl( read_file_as_string("general_template.tex") ); for (size_t i = 0; i < students_files.size(); ++i) { process_one_student_file_boolean(students_dir, students_files[i], general_tmpl, boolean_tpml); } return 0; } 




But wait, I hear the voice of the attentive reader. It will be nonsense to turn out - I suppose, a good half of the random formulas thus generated will turn out to be trivial! What is true is true - I don’t know about half, but even one randomly generated formula of the form \ varphi = x will considerably tarnish the reputation of our method. Let's learn how to check this. To do this, we simply calculate how many bundles and different variables are found in the formula, and demand that the variables meet all, and the bundles - at least two different. Add a formula traversal to BNode:



C ++ code
  void depth_first(function<void (const BNode * n)> do_with_node) { do_with_node(this); if (left != NULL) left->depth_first(do_with_node); if (right != NULL) right->depth_first(do_with_node); } 




and enter the validation of the formula for reasonableness:



C ++ code
 bool sanity_check(BNode * node) { vector<bool> vars_present(node->glob->VarsNo, false); vector<bool> connectors_present(node->glob->TypesNo, false); node->depth_first([&] (const BNode * n) { if (n->type == n->glob->VarType) { vars_present[ n->varnum ] = true; } else { connectors_present[ n->type ] = true; } }); return all_of( vars_present.begin(), vars_present.end(), [](bool b) {return b;} ) && (accumulate(connectors_present.begin(), connectors_present.end(), 0) > 2); } 




You may still want to check whether the formula is, say, always true, but I consciously decided not to do this - if a complex-looking formula suddenly turns out to be identically true, the more interesting this task will be for the student. And obvious subformulas of the type “x or not x” in our generator will not be obtained, because the variables are sorted out in turn, and not by chance.



Running the resulting program on a file with a list of students, we obtain a .tex file with quite adequately executed tasks (here is an example pdf compiled from such a file ).



Solutions





A skeptical reader at this place will reasonably argue: well, of course, you can generate over 9000 different tasks, but you will be tortured to check them later! Indeed, checking each student’s truth table is an exercise for very strong-minded people, whom I do not consider myself to be. Therefore, our program must be modified so that it can facilitate the verification process. It won't be possible to automate it completely (students will still hand over works written in free format by hand), so it will be enough just to do the most nasty part of this work in advance.



We get another LaTeX-template for the document with the answers:



LaTeX document template with answers
 {\footnotesize \subsection*{%1%,  %2%}     $%3%$: $$ %4% $$ } \pagebreak 




I, again, limit myself to a minimal example — let's just derive a truth table. To do this, go through all the possible values ​​of the variables, calculate the truth value of the formula and beautifully draw the result in TeX. Add two methods to the BNode class:



C ++ code
  bool increment_counter(vector<bool> & v) { for (int i=v.size()-1; i>=0; --i) { if (!v[i]) { v[i] = true; for (size_t j=i+1; j<v.size(); ++j) v[j] = false; return true; } } return false; } string latex_truthtable() { ostringstream os; vector<bool> counter(glob->VarsNo, false); os << "\\begin{array}{"; for(size_t i=0; i<counter.size(); ++i) os << 'c'; os << "|c}\n"; for(size_t i=0; i<counter.size(); ++i) os << glob->VarNames[i] << " & "; os << " \\\\\\hline\n"; do { for(size_t i=0; i<counter.size(); ++i) os << counter[i] << " & "; os << get_truth_value(counter) << "\\\\\n"; } while (increment_counter(counter)); os << "\\end{array}\n"; return os.str(); } 




and then add it to the process_one_student_file_boolean:



C ++ code
 void process_one_student_file_boolean(string dir, string fname, boost::format & general_tmpl, boost::format & problem_tmpl, boost::format & solution_tmpl) { BNode *node_bool; ostringstream s, ssolution; vector<string> students = readAllLinesFromFile(dir + "/" + fname + ".txt"); cout << "\t " << fname << endl; for (size_t i=0; i<students.size(); ++i) { if (students[i].size() == 0) continue; // empty line cout << "\t\t[ " << students[i] << " ]" << endl; do { node_bool = generate_tree(GBoolean, 2, 4); } while (!sanity_check(node_bool)); string group_string = "$" + fname + "$"; s << problem_tmpl % students[i] % group_string % node_bool->ToString(); ssolution << solution_tmpl % students[i] % group_string % node_bool->ToString() % node_bool->latex_truthtable(); delete node_bool; } ofstream ofs; open_for_writing(dir + "/" + fname + ".tex", ofs); ofs << general_tmpl % s.str() << endl; ofs.close(); open_for_writing(dir + "/" + fname + ".sol.tex", ofs); ofs << general_tmpl % ssolution.str() << endl; ofs.close(); } string students_dir = "2013"; vector<string> students_files = { "BoTR" }; int main(int argc, char *argv[]) { boost::format boolean_tpml( read_file_as_string("boolean_problem_minimal.tex") ); boost::format solution_tpml( read_file_as_string("boolean_solution_minimal.tex") ); boost::format general_tmpl( read_file_as_string("general_template.tex") ); for (size_t i = 0; i < students_files.size(); ++i) { process_one_student_file_boolean(students_dir, students_files[i], general_tmpl, boolean_tpml, solution_tpml); } return 0; } 




As a result, the corresponding decision file ( the same example, solutions ) is obtained in a pair to the task file ( example ), with which it becomes much easier to check.



Conclusion





And here's the result - half a day of work, and at the exit, as many tasks as you like with ready-made answers, everything is beautifully decorated and ready to be issued to students. If you are interested in how the real task turned out, here is an example of the final result . I will not post the answer file so as not to prompt the students once more - they are now solving this homework. I think that if my teaching at the HSE continues, this program will serve me more than once; The closest chance to apply it is tickets for the written exam in the same groups.



PS When I was preparing an article for Habr, I found a small bug in my formula generator; but did not correct. Exercise for the attentive reader: what formulas, in which, in principle, there is nothing bad, my generator will never be able to generate? (in addition to the notes on the selection of variables in order, which I have already done above)

Source: https://habr.com/ru/post/171279/



All Articles