пятница, 22 июля 2011 г.

Задача о счастливых билетах на шаблонах.

Итак, недавно захотелось попрактиковаться в извращениях на шаблонах.  Недолго думая, я решил сосчитать на них число счастливых билетов от 100 000 до 999 999. Назовем таковыми билеты, у  которых сумма первых трех цифр равна сумме последних .

При этом задача должна выполниться в compile-time на шаблонах. 

Первое что приходит в голову при решении такой задачи - организация простого цикла на шаблонах вида:


#include <stdio.h>

#define GET1(a)  //получение первой цифры числа
#define GET2(a)  //получение второй цифры числа
#define GET2(a)  //получение второй цифры числа
#define GET3(a)  //получение третьей цифры числа
#define GET4(a)  //получение четвёртой цифры числа
#define GET5(a)  //получение пятой цифры числа
#define GET6(a)  //получение шестой цифры числа

#define CHECK(a) ((GET1(a)+GET2(a)+GET3(a)==GET4(a)+GET5(a)+GET6(a))?1:0)

template<int A>
struct  get
{
  static const int val=CHECK(A)+get<A-1>::val;
}; 

template<>
struct  get<100000>
{
 static const int val=0;
};

int main(int argc,char ** argv) 
{
 printf("%d\n",get<999999>::val);
 return 0;
}

Но это решение не будет работать. Не будут работать и решения с разбитием числа сразу на отдельные символы и линейной итерацией  по ним. Причина в том, что происходит много инстанцирований "в глубину", т.е. не определив класс get<x> мы определяем классы get<x-1>, get<x-2>. и.т.п.  Современные компиляторы дают возможность делать много инстанцирований в глубину, в GCC к примеру был зашит предел в 500 который легко обходился указанием параметра -ftemplate-depth-1000000. Однако, это вызывало вылет компилятора. Очевидно, данное решение крайне неудачно.

Также в голову приходит очевидное решение: итерироваться не по числу, а по цифрам, что даёт более оптимальное решение. В коде оно будет выглядеть как:


#include <stdio.h>
#define GET(A) ((a0+a1+a2 == a3+a4+ A )?1:0)

template<
char a0,
char a1,
char a2,
char a3,
char a4,
>
struct get_lucky6
{
  static const int val=GET(9)+GET(8)+GET(7)+GET(6)+GET(5)+GET(4)+GET(3)+GET(2)+GET(1)+GET(0);
};


template<
char a0,
char a1,
char a2,
char a3,
char a4
>
struct get_lucky5
{
  static const int val=get_lucky6<a0,a1,a2,a3,a4>::val+get_lucky5<a0,a1,a2,a3,a4-1>::val;
};

template<
char a0,
char a1,
char a2,
char a3
>
struct get_lucky5<a0,a1,a2,a3,0>
{
  static const int val=get_lucky6<a0,a1,a2,a3,0,9>::val;
};

template<
char a0,
char a1,
char a2,
char a3
>
struct get_lucky4
{
  static const int val=get_lucky5<a0,a1,a2,a3,9>::val+get_lucky4<a0,a1,a2,a3-1>::val;
};

template<
char a0,
char a1,
char a2
>
struct get_lucky4<a0,a1,a2,0>
{
  static const int val=get_lucky5<a0,a1,a2,0,9>::val;
};

template<
char a0,
char a1,
char a2
>
struct get_lucky3
{
  static const int val=get_lucky4<a0,a1,a2,9>::val+get_lucky3<a0,a1,a2-1>::val;
};

template<
char a0,
char a1
>
struct get_lucky3<a0,a1,0>
{
  static const int val=get_lucky4<a0,a1,0,9>::val;
};

template<
char a0,
char a1
>
struct get_lucky2
{
  static const int val=get_lucky3<a0,a1,9>::val+get_lucky2<a0,a1-1>::val;
};


template<
char a0
>
struct get_lucky2<a0,0>
{
  static const int val=get_lucky3<a0,0,9>::val;
};

template<
char a0
>
struct get_lucky1
{
  static const int val=get_lucky2<a0,9>::val+get_lucky1<a0-1>::val;
};


template<>
struct get_lucky1<1>
{
  static const int val=get_lucky2<1,9>::val;
};


int main(int argc,char ** argv)
{
  printf("%d\n",get_lucky1<9>::val);
  return 0;
}

Здесь при помощи препроцессора развернут последний внутренний цикл, дабы чуть сократить время компиляции. Однако, этот код не дает успешного результата. При компиляции данного кода компилятор "съедает" 300+ Мб памяти, и после 4 часов компиляции не завершает свою работу.

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


#include <stdio.h>

#define GET(A1,A) ((a0+a1+a2 == a3+A1+ A )?1:0)
#define GETA1(A1) (GET(A1,9)+GET(A1,8)+GET(A1,7)+GET(A1,6)+GET(A1,5)+GET(A1,4)+GET(A1,3)+GET(A1,2)+GET(A1,1)+GET(A1,0))
#define GETA2     (GETA1(9)+GETA1(8)+GETA1(7)+GETA1(6)+GETA1(5)+GETA1(4)+GETA1(3)+GETA1(2)+GETA1(1)+GETA1(0))

#define GET0(A1,A) ((a0+a1+a2 == A1+ A )?1:0)
#define GET0A1(A1) (GET0(A1,9)+GET0(A1,8)+GET0(A1,7)+GET0(A1,6)+GET0(A1,5)+GET0(A1,4)+GET0(A1,3)+GET0(A1,2)+GET0(A1,1)+GET0(A1,0))
#define GET0A2     (GET0A1(9)+GET0A1(8)+GET0A1(7)+GET0A1(6)+GET0A1(5)+GET0A1(4)+GET0A1(3)+GET0A1(2)+GET0A1(1)+GET0A1(0))


template<
char a0,
char a1,
char a2,
char a3
>
struct get_lucky4
{
  static const int val=GETA2+get_lucky4<a0,a1,a2,a3-1>::val;
};

template<
char a0,
char a1,
char a2
>
struct get_lucky4<a0,a1,a2,0>
{
  static const int val=GET0A2;
};

template<
char a0,
char a1,
char a2
>
struct get_lucky3
{
  static const int val=get_lucky4<a0,a1,a2,9>::val+get_lucky3<a0,a1,a2-1>::val;
};

template<
char a0,
char a1
>
struct get_lucky3<a0,a1,0>
{
  static const int val=get_lucky4<a0,a1,0,9>::val;
};

template<
char a0,
char a1
>
struct get_lucky2
{
  static const int val=get_lucky3<a0,a1,9>::val+get_lucky2<a0,a1-1>::val;
};


template<
char a0
>
struct get_lucky2<a0,0>
{
  static const int val=get_lucky3<a0,0,9>::val;
};

template<
char a0
>
struct get_lucky1
{
  static const int val=get_lucky2<a0,9>::val+get_lucky1<a0-1>::val;
};


template<>
struct get_lucky1<1>
{
  static const int val=get_lucky2<1,9>::val;
};


int main(int argc,char ** argv)
{
  printf("%d\n",get_lucky1<9>::val);
  
  return 0;
}
Данный код "съедает" порядка 450Мб, компилируется около 1,5 мин и дает корректный результат.

Может показаться, что в таком случае задачу на шаблонах не стоит строить и лучше воспользоваться чистым препроцессором. Это не так. Скомпилировав код:


#include <stdio.h>
#define GET(A0,A1,A2,A3,A4,A5) ((A0+A1+A2 == A3+A4+ A5 )?1:0)
#define GET6(A0,A1,A2,A3,A4)   (GET(A0,A1,A2,A3,A4,0)+GET(A0,A1,A2,A3,A4,1)+GET(A0,A1,A2,A3,A4,2)+GET(A0,A1,A2,A3,A4,3)+GET(A0,A1,A2,A3,A4,4)+ \        GET(A0,A1,A2,A3,A4,5)+GET(A0,A1,A2,A3,A4,6)+GET(A0,A1,A2,A3,A4,7)+GET(A0,A1,A2,A3,A4,8)+GET(A0,A1,A2,A3,A4,9))
#define GET5(A0,A1,A2,A3)      (GET6(A0,A1,A2,A3,0)+GET6(A0,A1,A2,A3,1)+GET6(A0,A1,A2,A3,2)+GET6(A0,A1,A2,A3,3)+GET6(A0,A1,A2,A3,4)+ \                                GET6(A0,A1,A2,A3,5)+GET6(A0,A1,A2,A3,6)+GET6(A0,A1,A2,A3,7)+GET6(A0,A1,A2,A3,8)+GET6(A0,A1,A2,A3,9))
#define GET4(A0,A1,A2)         (GET5(A0,A1,A2,0)+GET5(A0,A1,A2,1)+GET5(A0,A1,A2,2)+GET5(A0,A1,A2,3)+GET5(A0,A1,A2,4)+GET5(A0,A1,A2,5)+ \                                GET5(A0,A1,A2,6)+GET5(A0,A1,A2,7)+GET5(A0,A1,A2,8)+GET5(A0,A1,A2,9))
#define GET3(A0,A1)            (GET4(A0,A1,0)+GET4(A0,A1,1)+GET4(A0,A1,2)+GET4(A0,A1,3)+GET4(A0,A1,4)+GET4(A0,A1,5)+GET4(A0,A1,6)       \                                +GET4(A0,A1,7)+GET4(A0,A1,8)+GET4(A0,A1,9)) 
#define GET2(A0)               (GET3(A0,0)+GET3(A0,1)+GET3(A0,2)+GET3(A0,3)+GET3(A0,4)+GET3(A0,5)+GET3(A0,6)+GET3(A0,7)+GET3(A0,8)+GET3(A0,9))
#define GET1                    (GET2(1)+GET2(2)+GET2(3)+GET2(4)+GET2(5)+GET2(6)+GET2(7)+GET2(8)+GET2(9))

const int lucky=GET1;
        
int main(int argc,char ** argv)
{
  printf("%d\n",lucky);
  
  return 0;
}
Мы при компиляции получим порядка минуты, однако компилятор при этом "съедает" около 952Мб памяти.

Вообще вычисление этой задачи в compile-time несёт мало преимуществ. Однако, это возможно.

При тестировании использовался компьютер со следующими параметрами:
Процессор:     Core 2 Duo 1.87GHz
RAM:              2Gb
Версия GCC: 4.4.3

SCons раздельная сборка + рекурсивное сканирование

Продолжаю репосты из своего старого блога. Также спасибо chexov - без него мне бы прикрутить highlight.js было бы сложнее.

Итак,  не так давно копаясь в Glob(), обнаружил что функция рекурсивного сканирования файлов по шаблонам отсутствует, что огорчает. Было найдено несколько велосипедов, но ни один не устроил (не работали или не могли исключать файлы). Также хотелось файлы сборки аккуратно сложить в папку build. В результате сделал такие два скрипта:

SConstruct


import platform
import os

def get_mingw_environment():
     mingw=ARGUMENTS.get('MINGW_TOOLCHAIN_PATH',"C:\\MinGW")
     env = Environment(tools = ['mingw'], ENV = os.environ)
     if mingw.endswith('\\') :
        env.PrependENVPath('PATH', mingw+'\\bin')
        env.PrependENVPath('LIB',  mingw+'\\lib')
     else:
        env.PrependENVPath('PATH', mingw+'bin')
        env.PrependENVPath('LIB',  mingw+'lib')
     return env

  
if platform.system() == 'Windows':
    use_mingw=ARGUMENTS.get('USE_MINGW',"NO")
    if use_mingw=="NO":
      env = Environment(ENV = os.environ)
    else:
      env=get_mingw_environment()
else:
     env = Environment(ENV = os.environ)


SConscript('SConstruct.me',build_dir='build',exports='env',duplicate=0)

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

Сам SConstruct.me уже осуществляет сканирование и сборку программы:


import os
import fnmatch

def merge_list(list1,list2):
    if list2 is not None:
       for item in list2:
           list1.append(item)

def is_matches(filename,include,excludes):
    match = False
    for pattern in include: 
       if fnmatch.fnmatchcase(filename, pattern):
           match=True
           break
    if match and excludes is not None:
        for pattern in excludes:
            if fnmatch.fnmatchcase(filename, pattern):
                match = False
                break
    return match

def merge_path(a,b):
     if (a=='.') or (a==''):
          return b
     if (b=='.') or (b==''):
       return a
     return a+"/"+b

def get_files(src_dir,abs_path,include,excludes=None):
    files = []
    for file in os.listdir(merge_path(abs_path,src_dir)):
       tpath=merge_path(src_dir,file)
       if os.path.isdir(merge_path(abs_path,tpath)):
          merge_list(files,get_files(tpath,abs_path,include,excludes))
       else:
          if (is_matches(tpath,include,excludes)):
              files.append(tpath)
    return files     
# Данная процедура осуществляет рекуррентный спуск из паки src_dir используя 
# шаблоны include и исключая файлы подпадающие под шаблоны exclude
def get_files_recursive(src_dir,include,excludes=None):
    return get_files(src_dir,Dir(src_dir).srcnode().abspath,include,excludes)
 
Import('env')
#  Здесь и "собирается новая программа, приведено чисто для примера"
env.Program('progr',get_files_recursive('.',["*.cpp"],["*/bomb.cpp"]), LIBS=['m'], LIBPATH=['.'],INCDIR=['.']);

Scons + MinGW

Решено при помощи http://www.conic.se/blog/posts/20/

 У самого свежего Scons есть небольшая проблема, а именно – он отказывается работать с MinGW, особенно если установлена Visual Studio, а на явное задание

Environment(tools = ['mingw'])
Выбрасывает ошибки. Однако на базе вышеприведенной ссылки, можно исправить такое поведение: Я добавил в скрипт возможность явного задания инструмента, которым можно это выполнить:

import platform
import os

def get_mingw_environment():
      mingw=ARGUMENTS.get('MINGW_TOOLCHAIN_PATH',"C:\\MinGW")
      env = Environment(tools = ['mingw'], ENV = os.environ)
      if mingw.endswith('\\') :
         env.PrependENVPath('PATH', mingw+'\\bin')
         env.PrependENVPath('LIB',  mingw+'\\lib')
      else:
         env.PrependENVPath('PATH', mingw+'bin')
         env.PrependENVPath('LIB',  mingw+'lib')
      return env

if platform.system() == 'Windows':
     use_mingw=ARGUMENTS.get('USE_MINGW',"NO")
     if use_mingw=="NO":
      env = Environment(ENV = os.environ)
     else:
      env=get_mingw_environment()
else:
      env = Environment(ENV = os.environ)


Использование:

scons USE_MINGW=YES MINGW_TOOLCHAIN_PATH=E:\MinGW
Первая переменная указывает на то, нужно ли использовать MinGW, вторая – путь к нему.