There are some good sources on-line to implement fast summation using binary splitting techniques. For example, Ch. 20, Jörg Arndt Book, (2004), Cheng et al. (2007) and papers from Haible and Papanikolaou (1997) and distributed with the CLN library source code. From this last article, the following notes apply to the evaluation of this kind of linearly convergent series (Type 1)
S = SUM(n=0,+oo,a(n)/b(n)*PROD(k=0,n,p(k)/q(k)))
where a(n), b(n), p(n), q(n) are integers with O(log N) bits. The most often used
case is that a(n), b(n), p(n), q(n) are polynomials in n with integer coefficients. Sum S is computed considering the following sequence of partial series with given two index bounds [n1, n2]
S(n1,n2) = SUM(n=n1,n2-1,a(n)/b(n)*PROD(k=n1,n,p(k)/q(k))
S(n1,n2) are not computed directly. Instead, the product of integers
P = p(n1) ... p(n2-1)
Q = q(n1) ... q(n2−1)
B = b(n1) ... b(n2-1)
T = B Q S
are computed recursively by binary splitting until n2 - n1 < 5 when these are computed directly. Choose an index nm in the middle of n1 and n2, compute the components Pl, Ql, Bl , Tl belonging to the interval n1 =< n < nm, compute the components Pr, Qr , Br, Tr belonging to the interval nm =< n < n2 and set these products and sums
P = Pl Pr
Q = Ql Qr,
B = Bl Br
T = Br Qr Tl + Bl Pl Tr
Finally, this algorithm is applied to n1 = 0 and n2 = nmax = O(N), and a final floating-point division
S = T/(B Q)
is performed.
The bit complexity of computing S with N bits of precision is O((log N)^2 M(N)) where M(N) is the bit complexity of the multiplication of two N-bit numbers.
A slightly modified but more complex series (Type 2) that is found in the last reference above can be also summed by binary splitting. It has an additional inner sum of rationals where c(n) and d(n) are integers with O(log N) bits
U = SUM(n=0,+oo,a(n)/b(n) * PROD(k=0,n,p(k)/q(k)) * SUM(m=0,n,c(m)/d(m)))
We consider these partial sums
U(n1,n2) = SUM(n=n1,n2-1,a(n)/b(n) * PROD(k=n1,n,p(k)/q(k)) * SUM(m=n1,n,c(m)/d(m)))
The algorithm is a variation of the above as follows.
P = p(n1) ... p(n2-1)
Q = q(n1) ... q(n2−1)
B = b(n1) ... b(n2-1)
T = B Q S
D = d(n1) ... d(n2-1)
C = D (c(n1)/d(n1) + ... + c(n2-1)/d(n2-1))
V = D B Q U
If n2 - n1 =< 4 these values are computed directly. If n2 - n1 > 4 they are computed by binary splitting. Choose an index nm in the middle of n1 and n2, compute the components Pl, Ql, Bl , Tl, Dl, Cl, Vl belonging to the interval n1 =< n < nm, compute the components Pr, Qr , Br, Tr, Dr, Cr, Vr belonging to the interval nm =< n < n2 and set these products and sums
P = Pl Pr
Q = Ql Qr
B = Bl Br
T = Br Qr Tl + Bl Pl Tr
D = Dl Dr
C = Cl Dr + Cr Dl
V = Dr Br Qr Vl + Dr Cl Bl Pl Tr + Dl Bl Pl Vr
At last, this algorithm is applied to n1 = 0 and n2 = nmax = O(N), and final floating point divisions are performed
S = T / (B Q)
V = U / (D B Q)
I have programmed both algorithms in Pari-GP and applied them to compute some mathematical constants using Chudnovsky's formula for Pi, this formula for Catalan Constant and more. (I have got more than 1000000 decimal digits in some cases under this platform). This code has been used to compute some difficult series as well.
I want to go one step ahead to accelerate some series by mixing binary splitting algorithm and levin-type sequence transformations. To do this I need to find the binary splitting relationships for a slight extension of these series.
W = SUM(n=0,+oo,a(n)/b(n) * PROD(k=0,n,p(k)/q(k)) * SUM(m=0,n,c(m)/d(m) * PROD(i=0,m,f(i)/g(i))))
It has has an additional product of rationals inside the inner sum where f(n) and g(n) are integers with O(log N) bits. These series are not hypergeometric but they are nested hypergeometric type sums. I think this algorithm might be derived from these partial series
W(n1,n2) = SUM(n=n1,n2-1,a(n)/b(n) * PROD(k=n1,n,p(k)/q(k)) * SUM(m=n1,n,c(m)/d(m) * PROD(i=n1,m,f(i)/g(i))))
I would very much appreciate if someone can derive the product and sum steps to bin-split this type of series.
I will leave the PARI-GP code for computing fast linearly convergent series of type 1 and 2 as explained. Use ?sumbinsplit for help. There are some Testing examples for Type 2 series as well. You can un-comment one of them and use
precision(-log10(abs(sumbinsplit(~F)[1]/s-1)),ceil(log10(Digits())));
to check it.
\\ ANSI COLOR CODES
{
DGreen = Dg = "\e[0;32m";
Brown = Br = "\e[0;33m";
DCyan = Dc = "\e[0;36m";
Purple = Pr = "\e[0;35m";
Gray = Gy = "\e[0;37m";
Red = Rd = "\e[0;91m";
Green = Gr = "\e[0;92m";
Yellow = Yw = "\e[0;93m";
Blue = Bl = "\e[0;94m";
Magenta = Mg = "\e[0;95m";
Cyan = Cy = "\e[0;96m";
Reset = "\e[0m";
White = Wh = "\e[0;97m";
Black = Bk = "\e[0;30m";
}
eps()={ my(e=1.); while(e+1. != 1., e>>=1); e; }
addhelp(eps,Str(Yw,"\n SMALLEST REAL NUMBER\n",Wh,"\n eps() ",Gr,"returns the minimum positive real number for current precision"));
log10(x) = if(x==0,log(eps()),log(x))/log(10);
Digits(n) = if(type(n) == "t_INT" && n > 0,default(realprecision,n); precision(1.), precision(1.));
addhelp(Digits,Str(Yw,"\n DIGITS\n",Wh,"\n Digits(n)",Gr," Sets global precision to",Wh," n",Gr," decimal digits.",Wh," Digits()",Gr," returns current global precision."));
addhelp(BinSplit2,Str(Yw,"\n SERIES BINARY SPLITTING (TYPE 2)\n\n",Wh,"BinSplit(~F,n1,n2)",Gr," for ",Wh,"F = [a(n),b(n),p(n),q(n),c(n),d(n)]",Gr," a vector of ",Br,"t_CLOSUREs",Gr," whose\n components are typically polynomials, computes by binary splitting method sums of type\n\n",Wh,"S2 = sum(n=n1,n2-1,a(n)/b(n)*prod(k=n1,n,p(k)/q(k))*sum(m=n1,n,c(m)/d(m)))\n\n",Gr,"Output: ",Wh," [P,Q,B,T,D,C,V]",Gr," integer valued algorithm computing parameters"));
addhelp(BinSplit1,Str(Yw,"\n SERIES BINARY SPLITTING (TYPE 1)\n\n",Wh,"BinSplit(~F,n1,n2)",Gr," for ",Wh,"F = [a(n),b(n),p(n),q(n)]",Gr," a vector of ",Br,"t_CLOSUREs",Gr," whose components\n are typically polynomials, computes by binary splitting method sums of type\n\n",Wh,"S1 = sum(n=n1,n2-1,a(n)/b(n)*prod(k=n1,n,p(k)/q(k)))\n\n",Gr,"Output: ",Wh,"[P,Q,B,T]",Gr," integer valued algorithm computing parameters"));
BinSplit2(~F, n1, n2) =
{
my( P = 1, Q = 1, B = 1, T = 0, D = 1, C = 0, V = 0,
LP, LQ, LB, LT, LD, LC, LV, RP, RQ, RB, RT, RD, RC, RV,
nm, tmp1 = 1, tmp2, tmp3 );
\\
\\ F = [a(n),b(n),p(n),q(n),c(n),d(n)]
\\
if( n2 - n1 < 5,
\\
\\ then
\\
for ( j = n1, n2-1,
LP = F[3](j);
LQ = F[4](j);
LB = F[2](j);
LD = F[6](j);
LC = F[5](j);
\\
tmp2 = LB * LQ;
tmp3 = LP * F[1](j) * tmp1;
T = T * tmp2 + tmp3;
C = C * LD + D * LC;
V = V * tmp2 * LD + C * tmp3;
P *= LP;
Q *= LQ;
B *= LB;
D *= LD;
tmp1 *= LP * LB;
),
\\
\\ else
\\
nm = (n1 + n2) >> 1;
\\
[RP,RQ,RB,RT,RD,RC,RV] = BinSplit2(~F, nm, n2);
[LP,LQ,LB,LT,LD,LC,LV] = BinSplit2(~F, n1, nm);
\\
tmp1 = RB * RQ;
tmp2 = LB * LP;
tmp3 = LC * RD;
\\
P = LP * RP;
Q = RQ * LQ;
B = LB * RB;
T = LT * tmp1 + RT * tmp2;
D = LD * RD;
C = RC * LD + tmp3;
V = RD * LV * tmp1 + ( RT * tmp3 + LD * RV ) * tmp2;
\\
\\ end if
);
return([P,Q,B,T,D,C,V]);
}
BinSplit1(~F, n1, n2) =
{
my( P = 1, Q = 1, B = 1, T = 0,
LP, LQ, LB, LT, RP, RQ, RB, RT,
tmp1 = 1, nm );
\\
\\ F = [a(n),b(n),p(n),q(n)]
\\
if( n2 - n1 < 5,
\\
\\ then
\\
for ( j = n1, n2-1,
LP = F[3](j);
LQ = F[4](j);
LB = F[2](j);
\\
T = T * LB * LQ + LP * F[1](j) * tmp1;
P *= LP;
Q *= LQ;
B *= LB;
\\
tmp1 *= LP * LB;
),
\\
\\ else
\\
nm = (n1 + n2) >> 1;
\\
[RP,RQ,RB,RT] = BinSplit1(~F, nm, n2);
[LP,LQ,LB,LT] = BinSplit1(~F, n1, nm);
\\
P = LP * RP;
Q = RQ * LQ;
B = LB * RB;
T = LT * RB * RQ + RT * LB * LP;
\\
\\ end if
);
return([P,Q,B,T]);
}
sumbinsplit(~F, n1 = 1, dgs = getlocalprec()) =
{
my( n = #F, P, Q, B, T, D, C, V, [a,b] = F[3..4] );
my( n2 = 1 + ceil(dgs*log(10)/log(abs(pollead(Pol(b(x),x))/pollead(Pol(a(x),x))))) );
\\
if ( n > 4, [P, Q, B, T, D, C, V] = BinSplit2(~F,n1,n2); return(1.*([V/D,T]/B/Q)),\
[P, Q, B, T] = BinSplit1(~F,n1,n2); return(1.*(T/B/Q)));
}
addhelp(sumbinsplit,Str(Yw,"\n LINEARLY CONVERGENT SERIES BINARY SPLITTING SUMMATION\n\n",Wh,"sumbinsplit( ~F, {n1 = 1}, {dgs = getlocalprec()} )\n\n",Gr,"for either ",Wh,"F = [a(n),b(n),p(n),q(n)] ",Gr,"or",Wh," F = [a(n),b(n),p(n),q(n),c(n),d(n)]",Gr," vectors of ",Br,"t_CLOSUREs",Gr," whose\n components are typically polynomials. It computes sums of type 1 or type 2 by binary splitting method\n\n (See BinSplit1, BinSplit2 help)\n\n",Wh,"n1",Gr," starting index (default 1),",Wh," dgs",Gr," result's floating precision\n\n",Yw,"OUTPUT:",Gr," either",Wh," S1",Gr," series value (Type 1) or ",Wh," [S2, S1]",Gr," series values [Type 2, Type1]"));
/* TESTINGS */
/*
Digits(100000);
a = n->1;
b = n->n;
p = n->n*(n<<1-1);
q = n->3*(3*n-1)*(3*n-2);
c = n->1;
d = n->n*(n<<1-1)<<1;
s = log(2)*(3*log(2)+Pi/2)/10-Pi^2/60;
F = [a,b,p,q,c,d];
*/
/*
Digits(100000);
s = -Pi*Catalan/2+33/32*zeta(3)+log(2)*Pi^2/24;
F = [n->1,n->n^2,n->n*(n<<1-1),n->3*(3*n-1)*(3*n-2),n->1,n->n*(n<<1-1)<<1];
\\ precision(-log10(abs(sumbinsplit(~F)[1]/s-1)),ceil(log10(Digits())));
*/
/*
Digits(10000);
a = n->1;
b = n->n<<1+1;
p = n->n<<1-1;
q = n->n<<3;
c = n->1;
d = n->(n<<1-1)^2;
s = Pi^3/648;
F = [a,b,p,q,c,d];
*/
/*
Digits(10000);
a = n->-1;
b = n->n^3;
p = n->-n;
q = n->(n<<1-1)<<1;
c = n->20*n-9;
d = n->n*(n<<1-1)<<1;
s = 2*Pi^4/75;
F = [a,b,p,q,c,d];
*/
/*
Digits(10000);
a = n->2;
b = n->n^2;
p = n->n;
q = n->(n<<1-1);
c = n->1;
d = n->n<<1-1;
s = 7*zeta(3)-2*Pi*Catalan;
F = [a,b,p,q,c,d];
*/
/*
Digits(10000);
a = n->1;
b = n->n^4;
p = n->n;
q = n->(n<<1-1)<<1;
c = n->36*n-17;
d = n->n*(n<<1-1)<<1;
s = 14*zeta(5)/9+5/18*Pi^2*zeta(3);
F = [a,b,p,q,c,d];
*/
/*
Digits(10000);
a = n->1;
b = n->n^2;
p = n->n;
q = n->(n<<1-1)<<1;
c = n->12*n-5;
d = n->n*(n<<1-1)<<1;
s = 5*zeta(3)/3;
F = [a,b,p,q,c,d];
*/
Making a close analysis of the nested hypergeometric series W and partial sums W(n1,n2) I have found the binary splitting relationships,
Write the inner hypergeometric sum in W(n1,n2) as
Z(n1,n2) = SUM(m=n1,n2-1,c(m)/d(m) * PROD(i=n1,m,f(i)/g(i)))
we have
W(n1,n2) = = SUM(n=n1,n2-1,a(n)/b(n) * PROD(k=n1,n,p(k)/q(k)) * Z(n1,n+1))
The algorithm is a deeper variation of the previous one. We have now 8 polynomial functions a(n), b(n), p(n), q(n), c(n), d(n), f(n), g(n). Set these products and sums to be computed by binary splitting
P = p(n1) ... p(n2-1)
Q = q(n1) ... q(n2−1)
B = b(n1) ... b(n2-1)
T = B Q S
D = d(n1) ... d(n2-1)
F = f(n1) ... f(n2-1)
G = g(n1) ... g(n2-1)
C = D G Z
V = D B Q G W
If n2 - n1 =< 4 these values are computed directly. If n2 - n1 > 4 they are computed recursively by binary splitting. Choose an index nm in the middle of n1 and n2, compute the components Pl, Ql, Bl , Tl, Dl, Cl, Vl, Fl, Gl belonging to the interval n1 =< n < nm, compute the components Pr, Qr , Br, Tr, Dr, Cr, Vr, Fr, Gr belonging to the interval nm =< n < n2 and set these products and sums
P = Pl Pr
Q = Ql Qr
B = Bl Br
T = Br Qr Tl + Bl Pl Tr
D = Dl Dr
F = Fl Fr
G = Gl Gr
C = Cl Dr Gr + Cr Dl Fl
V = Dr Br Qr Gr Vl + Dr Cl Bl Pl Gr Tr + Dl Bl Pl Fl Vr
Using auxiliary variables and factorizing, these 27 big integer products can be reduced to just 19. Finally, this algorithm is applied to n1 = 0 and n2 = nmax = O(N), and final floating point divisions are performed. Algorithm provides all 3 sums
S = T / (B Q)
Z = C / (D G)
W = V / (B Q D G)
I will code and test this algorithm to complement this answer. Many convergence acceleration methods (CAM) applied to some slowly convergent series have the structure of series W (for example, some classical CAMs like Salzer's, Gustavson's, sumalt() from Pari GP -Cohen, Rodriguez, Zagier-, Weniger's transformations and several Levin-type CAMs). I believe that the merge of BinSplit and Levin-type sequence transformations should provide a strong boost to this topic. We will see.
I am currently trying to refresh my Haskell knowledge by solving some Hackerrank problems.
For example:
https://www.hackerrank.com/challenges/maximum-palindromes/problem
I've already implemented an imperative solution in C++ which got accepted for all test cases. Now I am trying to come up with a pure functional solution in (reasonably idiomatic) Haskell.
My current code is
module Main where
import Control.Monad
import qualified Data.ByteString.Char8 as C
import Data.Bits
import Data.List
import qualified Data.Map.Strict as Map
import qualified Data.IntMap.Strict as IntMap
import Debug.Trace
-- precompute factorials
compFactorials :: Int -> Int -> IntMap.IntMap Int
compFactorials n m = go 0 1 IntMap.empty
where
go a acc map
| a < 0 = map
| a < n = go a' acc' map'
| otherwise = map'
where
map' = IntMap.insert a acc map
a' = a + 1
acc' = (acc * a') `mod` m
-- precompute invs
compInvs :: Int -> Int -> IntMap.IntMap Int -> IntMap.IntMap Int
compInvs n m facts = go 0 IntMap.empty
where
go a map
| a < 0 = map
| a < n = go a' map'
| otherwise = map'
where
map' = IntMap.insert a v map
a' = a + 1
v = (modExp b (m-2) m) `mod` m
b = (IntMap.!) facts a
modExp :: Int -> Int -> Int -> Int
modExp b e m = go b e 1
where
go b e r
| (.&.) e 1 == 1 = go b' e' r'
| e > 0 = go b' e' r
| otherwise = r
where
r' = (r * b) `mod` m
b' = (b * b) `mod` m
e' = shift e (-1)
-- precompute frequency table
initFreqMap :: C.ByteString -> Map.Map Char (IntMap.IntMap Int)
initFreqMap inp = go 1 map1 map2 inp
where
map1 = Map.fromList $ zip ['a'..'z'] $ repeat 0
map2 = Map.fromList $ zip ['a'..'z'] $ repeat IntMap.empty
go idx m1 m2 inp
| C.null inp = m2
| otherwise = go (idx+1) m1' m2' $ C.tail inp
where
m1' = Map.update (\v -> Just $ v+1) (C.head inp) m1
m2' = foldl' (\m w -> Map.update (\v -> liftM (\c -> IntMap.insert idx c v) $ Map.lookup w m1') w m)
m2 ['a'..'z']
query :: Int -> Int -> Int -> Map.Map Char (IntMap.IntMap Int)
-> IntMap.IntMap Int -> IntMap.IntMap Int -> Int
query l r m freqMap facts invs
| x > 1 = (x * y) `mod` m
| otherwise = y
where
calcCnt cs = cr - cl
where
cl = IntMap.findWithDefault 0 (l-1) cs
cr = IntMap.findWithDefault 0 r cs
f1 acc cs
| even cnt = acc
| otherwise = acc + 1
where
cnt = calcCnt cs
f2 (acc1,acc2) cs
| cnt < 2 = (acc1 ,acc2)
| otherwise = (acc1',acc2')
where
cnt = calcCnt cs
n = cnt `div` 2
acc1' = acc1 + n
r = choose acc1' n
acc2' = (acc2 * r) `mod` m
-- calc binomial coefficient using Fermat's little theorem
choose n k
| n < k = 0
| otherwise = (f1 * t) `mod` m
where
f1 = (IntMap.!) facts n
i1 = (IntMap.!) invs k
i2 = (IntMap.!) invs (n-k)
t = (i1 * i2) `mod` m
x = Map.foldl' f1 0 freqMap
y = snd $ Map.foldl' f2 (0,1) freqMap
main :: IO()
main = do
inp <- C.getLine
q <- readLn :: IO Int
let modulo = 1000000007
let facts = compFactorials (C.length inp) modulo
let invs = compInvs (C.length inp) modulo facts
let freqMap = initFreqMap inp
forM_ [1..q] $ \_ -> do
line <- getLine
let [s1, s2] = words line
let l = (read s1) :: Int
let r = (read s2) :: Int
let result = query l r modulo freqMap facts invs
putStrLn $ show result
It passes all small and medium test cases but I am getting timeout with large test cases.
The key to solve this problem is to precompute some stuff once at the beginning and use them to answer the individual queries efficiently.
Now, my main problem where I need help is:
The initital profiling shows that the lookup operation of the IntMap seems to be the main bottleneck. Is there better alternative to IntMap for memoization? Or should I look at Vector or Array, which I believe will lead to more "ugly" code.
Even in current state, the code doesn't look nice (by functional standards) and as verbose as my C++ solution. Any tips to make it more idiomatic? Other than IntMap usage for memoization, do you spot any other obvious problems which can lead to performance problems?
And is there any good sources, where I can learn how to use Haskell more effectively for competitive programming?
A sample large testcase, where the current code gets timeout:
input.txt
output.txt
For comparison my C++ solution:
#include <vector>
#include <iostream>
#define MOD 1000000007L
long mod_exp(long b, long e) {
long r = 1;
while (e > 0) {
if ((e & 1) == 1) {
r = (r * b) % MOD;
}
b = (b * b) % MOD;
e >>= 1;
}
return r;
}
long n_choose_k(int n, int k, const std::vector<long> &fact_map, const std::vector<long> &inv_map) {
if (n < k) {
return 0;
}
long l1 = fact_map[n];
long l2 = (inv_map[k] * inv_map[n-k]) % MOD;
return (l1 * l2) % MOD;
}
int main() {
std::string s;
int q;
std::cin >> s >> q;
std::vector<std::vector<long>> freq_map;
std::vector<long> fact_map(s.size()+1);
std::vector<long> inv_map(s.size()+1);
for (int i = 0; i < 26; i++) {
freq_map.emplace_back(std::vector<long>(s.size(), 0));
}
std::vector<long> acc_map(26, 0);
for (int i = 0; i < s.size(); i++) {
acc_map[s[i]-'a']++;
for (int j = 0; j < 26; j++) {
freq_map[j][i] = acc_map[j];
}
}
fact_map[0] = 1;
inv_map[0] = 1;
for (int i = 1; i <= s.size(); i++) {
fact_map[i] = (i * fact_map[i-1]) % MOD;
inv_map[i] = mod_exp(fact_map[i], MOD-2) % MOD;
}
while (q--) {
int l, r;
std::cin >> l >> r;
std::vector<long> x(26, 0);
long t = 0;
long acc = 0;
long result = 1;
for (int i = 0; i < 26; i++) {
auto cnt = freq_map[i][r-1] - (l > 1 ? freq_map[i][l-2] : 0);
if (cnt % 2 != 0) {
t++;
}
long n = cnt / 2;
if (n > 0) {
acc += n;
result *= n_choose_k(acc, n, fact_map, inv_map);
result = result % MOD;
}
}
if (t > 0) {
result *= t;
result = result % MOD;
}
std::cout << result << std::endl;
}
}
UPDATE:
DanielWagner's answer has confirmed my suspicion that the main problem in my code was the usage of IntMap for memoization. Replacing IntMap with Array made my code perform similar to DanielWagner's solution.
module Main where
import Control.Monad
import Data.Array (Array)
import qualified Data.Array as A
import qualified Data.ByteString.Char8 as C
import Data.Bits
import Data.List
import Debug.Trace
-- precompute factorials
compFactorials :: Int -> Int -> Array Int Int
compFactorials n m = A.listArray (0,n) $ scanl' f 1 [1..n]
where
f acc a = (acc * a) `mod` m
-- precompute invs
compInvs :: Int -> Int -> Array Int Int -> Array Int Int
compInvs n m facts = A.listArray (0,n) $ map f [0..n]
where
f a = (modExp ((A.!) facts a) (m-2) m) `mod` m
modExp :: Int -> Int -> Int -> Int
modExp b e m = go b e 1
where
go b e r
| (.&.) e 1 == 1 = go b' e' r'
| e > 0 = go b' e' r
| otherwise = r
where
r' = (r * b) `mod` m
b' = (b * b) `mod` m
e' = shift e (-1)
-- precompute frequency table
initFreqMap :: C.ByteString -> Map.Map Char (Array Int Int)
initFreqMap inp = Map.fromList $ map f ['a'..'z']
where
n = C.length inp
f c = (c, A.listArray (0,n) $ scanl' g 0 [0..n-1])
where
g x j
| C.index inp j == c = x+1
| otherwise = x
query :: Int -> Int -> Int -> Map.Map Char (Array Int Int)
-> Array Int Int -> Array Int Int -> Int
query l r m freqMap facts invs
| x > 1 = (x * y) `mod` m
| otherwise = y
where
calcCnt freqMap = cr - cl
where
cl = (A.!) freqMap (l-1)
cr = (A.!) freqMap r
f1 acc cs
| even cnt = acc
| otherwise = acc + 1
where
cnt = calcCnt cs
f2 (acc1,acc2) cs
| cnt < 2 = (acc1 ,acc2)
| otherwise = (acc1',acc2')
where
cnt = calcCnt cs
n = cnt `div` 2
acc1' = acc1 + n
r = choose acc1' n
acc2' = (acc2 * r) `mod` m
-- calc binomial coefficient using Fermat's little theorem
choose n k
| n < k = 0
| otherwise = (f1 * t) `mod` m
where
f1 = (A.!) facts n
i1 = (A.!) invs k
i2 = (A.!) invs (n-k)
t = (i1 * i2) `mod` m
x = Map.foldl' f1 0 freqMap
y = snd $ Map.foldl' f2 (0,1) freqMap
main :: IO()
main = do
inp <- C.getLine
q <- readLn :: IO Int
let modulo = 1000000007
let facts = compFactorials (C.length inp) modulo
let invs = compInvs (C.length inp) modulo facts
let freqMap = initFreqMap inp
replicateM_ q $ do
line <- getLine
let [s1, s2] = words line
let l = (read s1) :: Int
let r = (read s2) :: Int
let result = query l r modulo freqMap facts invs
putStrLn $ show result
I think you've shot yourself in the foot by trying to be too clever. Below I'll show a straightforward implementation of a slightly different algorithm that is about 5x faster than your Haskell code.
Here's the core combinatoric computation. Given a character frequency count for a substring, we can compute the number of maximum-length palindromes this way:
Divide all the frequencies by two, rounding down; call this the div2-frequencies. We'll also want the mod2-frequencies, which is the set of letters for which we had to round down.
Sum the div2-frequencies to get the total length of the palindrome prefix; its factorial gives an overcount of the number of possible prefixes for the palindrome.
Take the product of the factorials of the div2-frequencies. This tells the factor by which we overcounted above.
Take the size of the mod2-frequencies, or choose 1 if there are none. We can extend any of the palindrome prefixes by one of the values in this set, if there are any, so we have to multiply by this size.
For the overcounting step, it's not super obvious to me whether it would be faster to store precomputed inverses for factorials, and take their product, or whether it's faster to just take the product of all the factorials and do one inverse operation at the very end. I'll do the latter, because it just intuitively seems faster to do one inversion per query than one lookup per repeated letter, but what do I know? Should be easy to test if you want to try to adapt the code yourself.
There's only one other quick insight I had vs. your code, which is that we can cache the frequency counts for prefixes of the input; then computing the frequency count for a substring is just pointwise subtraction of two cached counts. Your precomputation on the input I find to be a bit excessive in comparison.
Without further ado, let's see some code. As usual there's some preamble.
module Main where
import Control.Monad
import Data.Array (Array)
import qualified Data.Array as A
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as M
import Data.Monoid
Like you, I want to do all my computations on cheap Ints and bake in the modular operations where possible. I'll make a newtype to make sure this happens for me.
newtype Mod1000000007 = Mod Int deriving (Eq, Ord)
instance Num Mod1000000007 where
fromInteger = Mod . (`mod` 1000000007) . fromInteger
Mod l + Mod r = Mod ((l+r) `rem` 1000000007)
Mod l * Mod r = Mod ((l*r) `rem` 1000000007)
negate (Mod v) = Mod ((1000000007 - v) `rem` 1000000007)
abs = id
signum = id
instance Integral Mod1000000007 where
toInteger (Mod n) = toInteger n
quotRem a b = (a * b^1000000005, 0)
I baked in the base of 1000000007 in several places, but it's easy to generalize by giving Mod a phantom parameter and making a HasBase class to pick the base. Ask a fresh question if you're not sure how and are interested; I'll be happy to do a more thorough writeup. There's a few more instances for Mod that are basically uninteresting and primarily needed because of Haskell's wacko numeric class hierarchy:
instance Show Mod1000000007 where show (Mod n) = show n
instance Real Mod1000000007 where toRational (Mod n) = toRational n
instance Enum Mod1000000007 where
toEnum = Mod . (`mod` 1000000007)
fromEnum (Mod n) = n
Here's the precomputation we want to do for factorials...
type FactMap = Array Int Mod1000000007
factMap :: Int -> FactMap
factMap n = A.listArray (0,n) (scanl (*) 1 [1..])
...and for precomputing frequency maps for each prefix, plus getting a frequency map given a start and end point.
type FreqMap = Map Char Int
freqMaps :: String -> Array Int FreqMap
freqMaps s = go where
go = A.listArray (0, length s)
(M.empty : [M.insertWith (+) c 1 (go A.! i) | (i, c) <- zip [0..] s])
substringFreqMap :: Array Int FreqMap -> Int -> Int -> FreqMap
substringFreqMap maps l r = M.unionWith (-) (maps A.! r) (maps A.! (l-1))
Implementing the core computation described above is just a few lines of code, now that we have suitable Num and Integral instances for Mod1000000007:
palindromeCount :: FactMap -> FreqMap -> Mod1000000007
palindromeCount facts freqs
= toEnum (max 1 mod2Freqs)
* (facts A.! sum div2Freqs)
`div` product (map (facts A.!) div2Freqs)
where
(div2Freqs, Sum mod2Freqs) = foldMap (\n -> ([n `quot` 2], Sum (n `rem` 2))) freqs
Now we just need a short driver to read stuff and pass it around to the appropriate functions.
main :: IO ()
main = do
inp <- getLine
q <- readLn
let freqs = freqMaps inp
facts = factMap (length inp)
replicateM_ q $ do
[l,r] <- map read . words <$> getLine
print . palindromeCount facts $ substringFreqMap freqs l r
That's it. Notably I made no attempt to be fancy about bitwise operations and didn't do anything fancy with accumulators; everything is in what I would consider idiomatic purely-functional style. The final count is about half as much code that runs about 5x faster.
P.S. Just for fun, I replaced the last line with print (l+r :: Int)... and discovered that about half the time is spent in read. Ouch! Seems there's still plenty of low-hanging fruit if this isn't fast enough yet.