[PYTHON] Competitive Pro Input Auto Generator and Object-Oriented Story

Overview

<!-* Also automatically generates (Python) source code to receive input->

Not only those who want to use the automatic generation function, but also those who want to know about programming that goes one step further from the competition pro. I think this content can be read by those who are studying object-oriented programming. The entire source code can be found at [here] 1.

What you can do: Automatically generate input test cases

If you prepare such an input macro →

Int(N,2,5)
Float(a,1,100,5) Perm(1,N+4)
Str([a-z]{3,2*N})
*N(2)

Input will come out →

output: 
4
54.81887 3 2 4 7 1 8 5 6
yyl
4.32497 4 1 6 5 3 8 7 2
yziuqiac
42.84603 3 2 4 7 8 6 5 1
vsjajs
65.07176 7 5 8 3 4 6 1 2
rbq

Various other things

Weighted graph:

Int(N,3,5) Int(M,N-1,N*(N-1)//2)
Graph1(N,M,0,1000)

output:
4 5
1 2 392
1 3 328
1 4 891
2 3 264
3 4 227

String:

Int(N,4,6) Int(M,4,6)
Str([.#]{M})
*N(1)

output:
4 5
###.#
#.#.#
#..##
.###.

etc.

If you are interested in the function itself, you can find a list in [here] 1.

<!-## What you can do: Automatically generate source code to receive input

Instead of spitting input, you can also use Python's input () function to generate source code that receives input:


``` -->

 * Since we have just defined the inner class so far, if you want to generate a lot of test cases at once or write it to a file, you need to code Python using this class.

## Realization method

 It has an object-oriented implementation.
 Roughly

 1. Class responsible for reading the entire macro (LineCollection)
 1. Class (Line) responsible for reading macros line by line
 1. The class that is responsible for the space delimiter in one line of the macro (Item and its subclasses)

 It is divided into. It is an image that i manages i + 1 and throws the necessary work (i = 1,2).
 All classes have the following methods in common:

 1. from_str (): Parses the input macro string and initializes the class.

 1. generate (): Generates a random input test case for the range it manages.

 In summary, it looks like the table below:

 ||LineCollection|Line|Item|
 |---|---|---|---|
 |from_str()|Loading the entire macro|Read one line|Reading one word|
 |generate()|Output generation for the entire macro|Output generation for one line|Output generation for one word|

 <!-- |generate()/generate_source()|Output generation for the entire macro|Output generation for one line|Output generation for one word| -->


 The point is

 * To use the whole, LineCollection.from_str (input macro) → generate ()

 <!-* To use the whole, LineCollection.from_str (input macro) → generate () / generate_source ()->

 * If you want to increase the number of new macro types, just define a subclass of the Item class that has the above three methods (no need to change Line or LineCollection).

 What a place, such as.

## Source code

 Specifically, we will check the source code.

 LineCollection

 ||<font color=green> LineCollection|Line|Item|
 |---|---|---|---|
 |from_str()|<font color=green>Loading the entire macro</font>|Read one line|Reading one word|
 |generate()|<font color=green>Output generation for the entire macro|Output generation for one line|Output generation for one word|

 <!-- |generate()/generate_source()|<font color=green>Output generation for the entire macro|Output generation for one line|Output generation for one word| -->


 I have a list of Lines for each line of the macro in self.ls.
 It's a bit complicated to support macros like "* N (1)", but what we're doing is

 * from_str () = Read entire macro: Initialize the Line corresponding to each line and put it in self.ls
 * generate () = Generate output for the entire macro: Generate output for each line in self.ls and return it with a newline character

 only.

```python
class LineCollection:
    def __init__(self, ls, s=None):
        """ls: list of Line
        """
        self.ls = ls
        self.s = s
    @classmethod
    def from_str(cls, s):
        lines = s.split("\n")
        i = 0
        ls = []
        for i in range(len(lines)):
            if lines[i].startswith("*"):
                name, num = lines[i][1:].split("(",1)
                num = int(num[:-1])
                ls.append((name, num))
            else:
                l = Line.from_str(lines[i])
                ls.append(l)
        return cls(ls, s)
    def generate(self):
        i = 0
        prv = 0
        output = []
        while i<len(self.ls):
            while i<len(self.ls) and not isinstance(self.ls[i], tuple):
                i += 1
            if i<len(self.ls) and isinstance(self.ls[i], tuple):
                m, num = self.ls[i]
                i += 1
            else:
                m = 0
                num = 0
            for j in range(prv, i-num-1):
                if isinstance(self.ls[j], tuple):
                    continue
                output.append(self.ls[j].generate())
            if num!=0:
                try:
                    m = Item.names[m]
                except KeyError:
                    raise ValueError("Undefined number of test cases:Isn't the specification of how many lines to see above wrong?")
                for _ in range(m):
                    for j in range(i-num-1, i-1):
                        if isinstance(self.ls[j], tuple):
                            continue
                        output.append(self.ls[j].generate())
            prv = i
        return "\n".join(output)

Line

LineCollection Line Item
from_str() Loading the entire macro Read one line Reading one word
generate() Output generation for the entire macro Output generation for one line Output generation for one word

self.l manages a list of Items that exist in the line you are looking at. Similar to the LineCollection,

I am doing that. Where we read the class name and initialize the Item, we get the class object from Python's built-in function globals ().

def evaluate_item(ss):
    cc, tmp = ss.split("(", 1)
    vv = tmp[:-1]
    return globals()[cc].from_str(vv)

class Line:
    def __init__(self, l, s=None):
        """
        correspond to a line of input file
        l: list of Item
        """
        self.l = l
        self.s = s
    @classmethod
    def from_str(cls, s):
        l = []
        for ss in s.split():
            l.append(evaluate_item(ss))
        return cls(l)
    def generate(self):
        return " ".join([item.generate() for item in self.l])

Item

LineCollection Line Item
from_str() Loading the entire macro Read one line Reading one word
generate() Output generation for the entire macro Output generation for one line Output generation for one word

Finally, Item, which is defined for each macro you want to support. In such a case, creating one base class and inheriting it is superior in terms of readability, maintainability, and coding efficiency. We have prepared the Item class as a base class as follows:

class Item:
    names = {}
    def __init__(self, s=None, **keys):
        self.s = s
    @classmethod
    def from_str(cls, s):
        pass
    def evaluate(self, s):
        for k in Item.names.keys():
            if k in s:
                s = s.replace(k, str(Item.names[k]))
        return eval(s)
    def generate(self):
        pass
    def __str__(self):
        if hasattr(self, "s"):
            return self.s

Item is a class that cannot be used as it is, so it is better to put a magic spell to represent it, but this time it is omitted. Just declaring that you have these methods has great benefits, such as making it easier for others to understand when adding features.

Under this, create a class for each macro you want to realize. For example, Int

class Int(Item):
    def __init__(self, name, low, high, s=None, **keys):
        """
        correspond to the input value between two spaces
        name: str
            name of variable
        low/high : str
            min / max (inclusive)
        """
        self.name = name
        self.low = low
        self.high = high
        self.keys = keys
        Item.__init__(self, s)
    @classmethod
    def from_str(cls, s):
        name, low, high = s.split(",")
        return cls(name, low, high, s=s)
    def generate(self):
        low, high = self.evaluate(self.low), self.evaluate(self.high)
        value = utils.rng.integers(low, high+1)
        Item.names[self.name] = value
        return str(value)

If it is a permutation of 1 ... N

class Perm(Item):
    """permutation of [low, low+1, ..., high-1, high]
    """
    def __init__(self, low, high, s=None):
        self.low = low
        self.high = high
        self.s = s
    @classmethod
    def from_str(cls, s):
        low, high = s.split(",")
        return cls(low, high, s=s)
    def generate(self):
        low, high = self.evaluate(self.low), self.evaluate(self.high)
        return " ".join(map(str, (utils.rng.permutation(high-low+1) + low).tolist()))

It is shaped like. Where random numbers are needed, they are thrown into an externally defined random number generator:

utils.py


import numpy as np
SEED = 0
rng = np.random.default_rng(SEED)

When using random numbers, it is correct not to call the function each time, but to prepare random number correctors (multiple if necessary) and use them properly according to the scope. However, in this case, it depends on whether it is desirable to fix the random number (to obtain reproducibility) or to change the result for each execution, so it may be necessary to rewrite it depending on the use case. ..

Other macros will be supported one by one in the same way. For example, if it is a graph, it is realized by using functions such as networkx random graph generation, and if it is a character string, a character string that matches the regular expression pattern by rstr is randomly generated. What to do anyway

It's just a matter of implementing, so the effort to add functions is reduced!

Summary

<!-* Also automatically generates (Python) source code to receive input->

TODO

Recommended Posts

Competitive Pro Input Auto Generator and Object-Oriented Story
Competitive Pro with Python and VSCode-Simplification of standard input and automation of sample case judgment-