1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
|
#!/usr/bin/env perl
my $copyright = <<'COPYRIGHT';
# Copyright (c) 2021 by Christian Jaeger <copying@christianjaeger.ch>
# This is free software. See the file COPYING.md that came bundled
# with this file.
COPYRIGHT
=pod
L<The Weekly Challenge - 113|https://perlweeklychallenge.org/blog/perl-weekly-challenge-113/>,
TASK #1: Represent Integer
You are given a positive integer $N and a digit $D.
Write a script to check if $N can be represented as a sum of positive
integers [all] having $D at least once [in their decimal
representation]. If check passes print 1 otherwise 0.
=head1 NOTE
There is a Haskell version of this script in the file
L<113-1-represent_integer_haskell.hs>.
There is a L<blog post about this|http://functional-perl.org/docs/blog/perl-weekly-challenges-113.xhtml>.
=cut
use strict;
use utf8;
use warnings;
use warnings FATAL => 'uninitialized';
use experimental 'signatures';
use feature 'current_sub'; # __SUB__
my ($mydir, $myname);
BEGIN {
$0 =~ /(.*?)([^\/]+)\z/s or die "?";
($mydir, $myname) = ($1, $2);
}
use lib "$mydir/../../lib";
use FunctionalPerl v0.72.65;
use FP::Docstring;
use FP::Show;
use FP::List;
use FP::PureArray;
use FP::Lazy;
use FP::Either ":all";
use FP::Predicates qw(is_defined);
use Chj::TEST ":all";
use Chj::time_this;
our $verbose = $ENV{VERBOSE};
sub maybe_choose_brute ($N, $ns) {
__ 'Choose a combination of numbers from $ns (repetitions allowed)
that added together equal $N; undef if not possible. This
solution is brute force in that it is picking additional
numbers from the left end of $ns, one after another,
depth-first.';
sub ($chosen) {
my $check = __SUB__;
warn "check (brute): " . show($chosen) if $verbose;
my $sum = $chosen->sum;
if ($sum == $N) {
$chosen
} elsif ($sum > $N) {
undef
} else {
$ns->any(
sub ($n) {
$check->(cons($n, $chosen))
}
)
}
}
->(null)
}
sub maybe_choose_optim_1 ($N, $ns) {
__ 'Choose a combination of numbers from $ns (repetitions allowed)
that added together equal $N; undef if not possible. This
solution uses a hashtable to check for each additional number;
i.e. it tries to minimize the number of numbers taken from
$ns (it is still searching depth-first).';
my %ns = map { $_ => 1 } $ns->values;
sub ($chosen) {
my $check = __SUB__;
warn "check (optim 1): " . show($chosen) if $verbose;
my $sum = $chosen->sum;
my $missing = $N - $sum;
if (not $missing) {
$chosen
} elsif ($missing < 0) {
undef
} else {
if (exists $ns{$missing}) {
cons $missing, $chosen
} else {
$ns->any(
sub ($n) {
$check->(cons($n, $chosen))
}
)
}
}
}
->(null)
}
sub maybe_choose_optim_2 ($N, $_ns) {
__ 'Choose a combination of numbers (repetitions allowed) from $ns
which must be sorted in decrementing order that added together
equal $N; undef if not possible. This solution does a
breadth-first search (and uses the hashtable check to see if
there will be a match with the next level like
maybe_choose_optim_1)';
# We want to use lazy evaluation to allow for the descriptive
# solution below, thus turn the purearray to a lazy list:
my $ns = $_ns->stream;
my %ns = map { $_ => 1 } $_ns->values;
sub ($chosen) {
my $check = __SUB__;
warn "check (optim 2): " . show($chosen) if $verbose;
# Given an additional choice of a number $n (out of $ns) on
# top of $chosen, decide whether there's a solution either
# with the given numbers or when adding one more missing
# number by looking at %ns; or whether the chosen numbers are
# adding up to too much already (in which case undef is
# returned), or the search needs to resume via recursively
# calling $check. The latter case is not carried out
# immediately, but returned as a lazy term (a promise), to
# allow to delay diving deeper into the next recursion level
# to *after* checking all numbers in the current level
# (breadth-first search).
# Using FP::Either's `Right` to indicate an immediate
# solution, `Left` to indicate a case that needs recursion
# (and only potentially yields a result), undef to signify a
# dead end.
my $decide = sub ($n) {
warn "decide: checking $n on top of " . show($chosen) if $verbose;
my $chosen = cons $n, $chosen;
my $missing = $N - ($chosen->sum);
if (not $missing) {
Right $chosen
} elsif ($missing < 0) {
undef
} else {
if (exists $ns{$missing}) {
Right cons($missing, $chosen)
} else {
Left lazy {
$check->($chosen)
}
}
}
};
# Since $ns are sorted in decrementing order, if $decide
# returns undef, any subsequent number will fail, too, so we
# can stop further checks; `take_while` will only take the
# results up to that point.
# Since $ns is a stream (a lazily computed list), the
# following `map` and `take_while` steps are lazy, too;
# $decide will never be evaluated for $n's that are smaller
# (coming further along in the reverse-ordered $ns) than any
# $n that can lead to a solution.
my $decisions = $ns->map($decide)->take_while(\&is_defined);
# Check for immediate solutions (solutions on our level)
# first, if that fails, get and evaluate the promises to
# recurse (go deeper):
my $solutions = rights $decisions;
my $recursions = lefts $decisions;
unless ($solutions->is_null) {
$solutions->first
} else {
$recursions->any(\&force)
}
}
->(null)
}
# (But there may be smarter algorithms.)
sub valid_numbers ($N, $D) {
purearray grep {/$D/} (1 .. $N)
}
# You can explicitely choose the algorithm it via setting
# `MAYBE_CHOOSE` env var to its name, or by passing its reference as
# the 4th argument to maybe_representable.
my $MAYBE_CHOOSE = do {
if (my $str = $ENV{MAYBE_CHOOSE}) {
+{
maybe_choose_brute => \&maybe_choose_brute,
maybe_choose_optim_1 => \&maybe_choose_optim_1,
maybe_choose_optim_2 => \&maybe_choose_optim_2
}->{$str}
or die "invalid MAYBE_CHOOSE value"
} else {
undef
}
};
sub maybe_representable ($N, $D, $prefer_large = 1,
$maybe_choose = $MAYBE_CHOOSE)
{
__ 'Returns the numbers containing $D that sum up to $N, or undef.
If $prefer_large is true, tries to use large numbers,
otherwise small (which is (much) less efficient).';
my $ns = valid_numbers($N, $D);
$maybe_choose
//= ($prefer_large and not $ENV{NO_OPTIM})
? \&maybe_choose_optim_2
: \&maybe_choose_brute;
$maybe_choose->($N, $prefer_large ? $ns->reverse : $ns)
}
TEST { maybe_representable 25, 7 } undef;
TEST { maybe_representable 24, 7 } list(7, 17);
TEST { maybe_representable 200, 9 } list(9, 191);
TEST { maybe_representable 200, 9, 0 }
list(29, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9);
# The comments to the right in the following show the results that
# maybe_choose_optim_1 gave:
TEST { maybe_representable 200, 8 } list(18, 182); # list(8, 8, 184);
TEST { maybe_representable 200, 8, 0 }
list(8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8);
TEST { maybe_representable 200, 7 } list(27, 173); # list(7, 7, 7, 179);
TEST { maybe_representable 200, 6 } list(36, 164); # list(6, 6, 6, 6, 176);
unless ($ENV{NO_EXPENSIVE_TESTS}) {
TEST {
time_this { maybe_representable 20000, 8 }
}
list(18, 19982); # list(8, 8, 19984);
TEST {
time_this { maybe_representable 40000, 8 }
}
list(18, 39982); # list(8, 8, 39984);
TEST {
time_this { maybe_representable 40000, 6 }
}
list(36, 39964); # list(6, 6, 6, 6, 39976);
}
# ----------------------------------------------------------------------
sub help {
print "Usage: $0 --repl | --test\n";
exit 1
}
&{
@ARGV
? {
"--repl" => sub {
require FP::Repl::Trap;
FP::Repl::repl();
},
"--test" => sub {
run_tests __PACKAGE__;
}
}->{ $ARGV[0] } // \&help
: \&help
};
|