在我的评论中,我说解析相当容易。这是如何做到的。由于问题缺乏正确的文件格式规范,我将假设以下内容:
该文件由具有值的属性组成:
document ::= property*
property ::= word "(" value ("," value)* ")"
值是包含用逗号分隔的数字或单个单词的双引号字符串:
value ::= '"' ( word | number ("," number)* ) '"'
空格、反斜杠和注释无关紧要。
这是一个可能的实现;我不会详细解释如何编写一个简单的解析器。
package Parser;
use strict; use warnings;
sub parse {
my ($data) = @_;
# perform tokenization
pos($data) = 0;
my $length = length $data;
my @tokens;
while(pos($data) < $length) {
next if $data =~ m{\G\s+}gc
or $data =~ m{\G\\}gc
or $data =~ m{\G/[*].*?[*]/}gc;
if ($data =~ m/\G([",()])/gc) {
push @tokens, [symbol => $1];
} elsif ($data =~ m/\G([0-9]+[.][0-9]+)/gc) {
push @tokens, [number => 0+$1];
} elsif ($data =~ m/\G(\w+)/gc) {
push @tokens, [word => $1];
} else {
die "unreckognized token at:\n", substr $data, pos($data), 10;
}
}
return parse_document(\@tokens);
}
sub token_error {
my ($token, $expected) = @_;
return "Wrong token [@$token] when expecting [@$expected]";
}
sub parse_document {
my ($tokens) = @_;
my @properties;
push @properties, parse_property($tokens) while @$tokens;
return @properties;
}
sub parse_property {
my ($tokens) = @_;
$tokens->[0][0] eq "word"
or die token_error $tokens->[0], ["word"];
my $name = (shift @$tokens)->[1];
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '('
or die token_error $tokens->[0], [symbol => '('];
shift @$tokens;
my @vals;
VAL: {
push @vals, parse_value($tokens);
if ($tokens->[0][0] eq 'symbol' and $tokens->[0][1] eq ',') {
shift @$tokens;
redo VAL;
}
}
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq ')'
or die token_error $tokens->[0], [symbol => ')'];
shift @$tokens;
return [ $name => @vals ];
}
sub parse_value {
my ($tokens) = @_;
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '"'
or die token_error $tokens->[0], [symbol => '"'];
shift @$tokens;
my $value;
if ($tokens->[0][0] eq "word") {
$value = (shift @$tokens)->[1];
} else {
my @nums;
NUM: {
$tokens->[0][0] eq 'number'
or die token_error $tokens->[0], ['number'];
push @nums, (shift @$tokens)->[1];
if ($tokens->[0][0] eq 'symbol' and $tokens->[0][1] eq ',') {
shift @$tokens;
redo NUM;
}
}
$value = \@nums;
}
$tokens->[0][0] eq "symbol" and $tokens->[0][1] eq '"'
or die token_error $tokens->[0], [symbol => '"'];
shift @$tokens;
return $value;
}
现在,我们得到以下数据结构作为输出Parser::parse
:
(
["Student_name", "Eric"],
["scoreA", [10, 20, 30, 40]],
["scoreB", [15, 30, 45, 50, 55]],
[
"final",
[12.23, 19, 37.88, 45.98, 60],
[7, 20.11, 24.56, 45.66, 57.88],
[5, 15.78, 22.88, 40.9, 57.99],
[10, 16.87, 26.99, 38.99, 40.66],
],
["Student_name", "Liy"],
["scoreA", [5, 10, 20, 60]],
["scoreB", [25, 30, 40, 55, 60]],
[
"final",
[2.23, 15, 37.88, 45.98, 70],
[10, 28.11, 34.56, 45.66, 57.88],
[8, 19.78, 32.88, 40.9, 57.66],
[10, 27.87, 39.99, 59.99, 78.66],
],
...,
)
下一步,我们要将其转换为嵌套哈希,以便我们拥有结构
{
Eric => {
scoreA => [...],
scoreB => [...],
final => [[...], ...],
},
Liy => {...},
...,
}
所以我们简单地通过这个小子运行它:
sub properties_to_hash {
my %hash;
while(my $name_prop = shift @_) {
$name_prop->[0] eq 'Student_name' or die "Expected Student_name property";
my $name = $name_prop->[1];
while( @_ and $_[0][0] ne 'Student_name') {
my ($prop, @vals) = @{ shift @_ };
if (@vals > 1) {
$hash{$name}{$prop} = \@vals;
} else {
$hash{$name}{$prop} = $vals[0];
}
}
}
return \%hash;
}
所以我们有主要代码
my $data = properties_to_hash(Parser::parse( $file_contents ));
现在我们可以进入问题的第 2 部分:计算你的分数。也就是说,一旦你明确了你需要做什么。
编辑:双线性插值
令f为返回某个坐标处的值的函数。如果我们在这些坐标上有一个值,我们可以返回它。否则,我们使用下一个已知值执行双线性插值。
双线性插值[ 1 ]的公式为:
f(x, y) = 1/( (x_2 - x_1) · (y_2 - y_1) ) · (
f(x_1, y_1) · (x_2 - x) · (y_2 - y)
+ f(x_2, y_1) · (x - x_1) · (y_2 - y)
+ f(x_1, y_2) · (x_2 - x) · (y - y_1)
+ f(x_2, y_2) · (x - x_1) · (y - y_1)
)
现在,在第一个轴上表示表格中scoreA
数据点的位置,在第二个轴上表示位置。我们必须做到以下几点:final
scoreA
- 断言请求的值
x, y
在边界内
- 获取下一个较小和下一个较大的位置
- 执行插值
.
sub f {
my ($data, $x, $y) = @_;
# do bounds check:
my ($x_min, $x_max, $y_min, $y_max) = (@{$data->{scoreA}}[0, -1], @{$data->{scoreB}}[0, -1]);
die "indices ($x, $y) out of range ([$x_min, $x_max], [$y_min, $y_max])"
unless $x_min <= $x && $x <= $x_max
&& $y_min <= $y && $y <= $y_max;
要获取拳击指数x_1, x_2, y_1, y_2
,我们需要遍历所有可能的分数。我们还将记住x_i1, x_i2, y_i1, y_i2
底层数组的物理索引。
my ($x_i1, $x_i2, $y_i1, $y_i2);
for ([$data->{scoreA}, \$x_i1, \$x_i2], [$data->{scoreB}, \$y_i1, \$y_i2]) {
my ($scores, $a_i1, $a_i2) = @$_;
for my $i (0 .. $#$scores) {
if ($scores->[$i] <= $x) {
($$a_i1, $$a_i2) = $i == $#$scores ? ($i, $i+1) : ($i-1, $i);
last;
}
}
}
my ($x_1, $x_2) = @{$data->{scoreA}}[$x_i1, $x_i2];
my ($y_1, $y_2) = @{$data->{scoreB}}[$y_i1, $y_i2];
现在可以按照上面的公式进行插值,但是在已知索引处的每次访问都可以变成通过物理索引的访问,所以f(x_1, y_2)
会变成
$final->[$x_i1][$y_i2]
详细解释sub f
sub f { ... }
用 name 声明一个 sub f
,尽管这可能是一个坏名字。bilinear_interpolation
可能是一个更好的名字。
my ($data, $x, $y) = @_
声明我们的 sub 接受三个参数:
$data
, 包含字段和的哈希引用scoreA
,它们是数组引用。scoreB
final
$x
,沿 -scoreA
轴需要插值的位置。
$y
,沿 -scoreB
轴需要插值的位置。
接下来,我们要断言 和 的位置$x
是$y
有效的,也就是在边界内。中的第一个值$data->{scoreA}
是最小值;最大值在最后一个位置(索引-1
)。为了同时获得两者,我们使用数组 slice。切片一次访问多个值并返回一个列表,例如@array[1, 2]
. 因为我们使用使用引用的复杂数据结构,所以我们必须取消引用$data->{scoreA}
. 这使得切片看起来像@{$data->{scoreA}}[0, 1]
.
现在我们有了$x_min
and$x_max
值,除非请求的值$x
在最小/最大值定义的范围内,否则我们会抛出错误。这是真的,当
$x_min <= $x && $x <= $x_max
如果要么超出范围,$x
要么$y
超出范围,我们会抛出一个错误,显示实际范围。所以代码
die "indices ($x, $y) out of range ([$x_min, $x_max], [$y_min, $y_max])"
例如,可以抛出一个错误,例如
indices (10, 500) out of range ([20, 30], [25, 57]) at script.pl line 42
在这里我们可以看到 for 的值$x
太小,而$y
太大。
下一个问题是找到相邻值。假设scoreA
成立[1, 2, 3, 4, 5]
和,我们要选择和$x
的值。但是因为我们可以稍后使用一些漂亮的技巧,所以我们宁愿记住相邻值的位置,而不是值本身。所以这会在上面的例子中给出和(记住箭头是从零开始的)。3.7
3
4
2
3
我们可以通过遍历数组的所有索引来做到这一点。当我们找到一个 ≤ 的值时$x
,我们会记住索引。Eg3
是第一个 ≤ 的值$x
,所以我们记住了索引2
。对于下一个更高的值,我们必须有点谨慎:显然,我们可以只取下一个索引,所以2 + 1 = 3
. 但现在假设$x
是5
。这通过了边界检查。≤ 的第一个值是$x
value 5
,所以我们可以记住 position 4
。但是,在 position 没有条目5
,所以我们可以使用当前索引本身。因为这会导致稍后除以零,所以我们最好记住位置3
和4
(值4
和5
)。
表示为代码,即
my ($x_i1, $x_i2);
my @scoreA = @{ $data->{scoreA} }; # shortcut to the scoreA entry
for my $i (0 .. $#scores) { # iterate over all indices: `$#arr` is the last idx of @arr
if ($scores[$i] <= $x) { # do this if the current value is ≤ $x
if ($i != $#scores) { # if this isn't the last index
($x_i1, $x_i2) = ($i, $i+1);
} else { # so this is the last index
($x_i1, $x_i2) = ($i-1, $i);
}
last; # break out of the loop
}
}
在我的原始代码中,我选择了一个更复杂的解决方案,以避免复制粘贴相同的代码来查找$y
.
因为我们还需要这些值,所以我们通过带有索引的切片来获取它们:
my ($x_1, $x_2) = @{$data->{scoreA}}[$x_i1, $x_i2];
现在我们有了所有周围的值$x1, $x_2, $y_1, $y_2
,这些值定义了我们要在其中执行双线性插值的矩形。数学公式很容易翻译成 Perl:只需选择正确的运算符 ( *
,而不是·
乘法),变量前面需要美元符号。
我使用的公式是递归的: f的定义是指它自己。这意味着一个无限循环,除非我们做一些思考并打破递归。f表示某个位置的值。在大多数情况下,这意味着插值。但是,如果$x
和$y
分别等于 和 中的值scoreA
,scoreB
我们不需要双线性插值,可以final
直接返回条目。
这可以通过检查$x
和$y
是否都是它们的数组的成员,并提前返回来完成。$x_1, ..., $y_2
或者我们可以使用所有都是数组成员的事实。我们不需要使用我们知道不需要插值的值进行递归,而是进行数组访问。这就是我们保存索引$x_i1, ..., $y_i2
的目的。因此,无论原始公式所说f(x_1, y_1)
或类似的地方,我们都写出等价的$data->{final}[$x_i1][$y_i2]
.