提问人:Dillon Davis 提问时间:2/6/2023 更新时间:2/11/2023 访问量:145
在预先存在的数据中对数据进行编码的最佳算法
Optimal algorithm for encoding data within pre-existing data
问:
假设我们有一些现有的随机数据,均匀分布,已经写入了某种介质。还可以说,比特的写入是一种破坏性的动作,而对于比特来说,它是非破坏性的。这可能类似于在穿孔卡上打孔或在电路中烧掉保险丝以硬编码 .在进行初始写入之后,假设我们现在希望将其他数据写入该介质,并使用一些特殊的编码,使我们能够在未来检索这些新数据,而无需对已经破坏性写入介质的预先存在的数据有特殊了解。预先存在的数据本身不需要保持可检索性,只需要保持新数据的可检索性。1
0
1
还要假设新数据本身是随机的,或者至少已经被压缩为有效的随机数据。
显然,我们不能指望超过原始存储容量的~50%,因为只有大约一半是可写的。问题在于试图将效率尽可能接近这个极限。
我有两个相当简单的编码,效率看似合理,但它们似乎不是最佳的。为了便于解释,我将假设介质是带有孔(表示 a)或间隙(表示 a)的纸带,每隔一段时间就会出现。1
0
编码 A
在磁带上以偶数偏移量对 a 位进行编码,在奇数偏移处对 a 位进行编码。1
gap
0
gap
Offset: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Tape: G G H H H H H G H H H G H H H H G H H H
| | | | |
New Data: 1 0 0 0 1
编码 B
用 a 后跟 another 对位进行编码,用 a 后跟 a 对 .1
gap
gap
0
gap
hole
Offset: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
Tape: H G G H H H H G H H H G G H H H G H G H
\/ \/ \/ \/ \/
New Data: 1 0 1 0 0
平均而言,这两种方法都可以对原始存储容量的 25% 的数据进行编码。这似乎很合理,至少对我来说是这样——一开始只有一半的磁带是 s,我们写的任何 s 都会丢失信息(因为我们在以后尝试解码时无法知道它们是否预先存在),所以四分之一 - 25%。乍一看似乎是最佳的。gap
hole
然而,困扰我的是,事实似乎并非如此。我有一个稍微复杂的编码,它始终打破这个阈值。
编码 C
从本质上讲,我们将位编码为奇数长度的 s 游程,将比特编码为偶数长度的 s 游程。所以会代表 while 或 would 代表 .1
hole
0
hole
HHG
0
HG
HHHG
1
然而,这本身并不足以跨越 25% 的阈值,因此我们添加了一个优化。当我们在 s 的运行和终止之后看到两个连续的 s 时,我们将它们视为 .这背后的逻辑是,如果我们想对 a 进行编码,我们可以直接打出两个间隙中的第一个,产生 ,编码 a 。由于我们没有这样做,我们可以假设我们需要对gap
hole
gap
gaps
0
1
HG
1
0
通过这种优化,编码 C 达到了稳定的 26.666 +/- 0.005 存储效率,始终高于理论值 25%。我还有一些非常棘手的窥视孔优化,我可以将其附加到这种编码上,我怀疑这会将其效率提高到 28-30%,但感觉我已经想多了。
编码 D?
这并不是真正的编码,而是我最后一个想法,它可以改进上述任何编码。考虑一种编码,以及一些确定性+可逆的转换,它以某种任意方式改变数据。假设我们在要编码的数据中预置一点,然后对其进行编码。假设我们也改变了我们的数据,预置了一点,然后对其进行编码。然后,我们可以比较两者,看看哪个碰巧编码了更多的数据(效率更高),然后选择那个。总体上,改进会很小,取决于统计变化,我们确实牺牲了一点作为指标,但只要数据足够大,平均节省的比特就会超过损失的单个比特。这可以概括 - 将数据划分为几个分区,并在 2 个或更多编码之间进行选择,以随机最适合的编码为准,但这不是重点。总的来说,改进会很小,但它仍然应该是一个直接的改进 - 表明基本编码不是最佳的。E(x)
T(x)
0
E('0' + DATA)
1
E('1' + T(DATA))
E(x)
回顾一下 - 我正在寻找最有效(在一般情况下)无损编码,用于将数据写入已经使用纯随机数据半破坏性(仅 1)写入的介质。我真的相信最佳解决方案的效率在 30-50% 之间,但我正在苦苦挣扎。我希望有人可以分享什么是最佳编码,或者阐明围绕这个主题的相关文献/理论。
旁注:在试图更好地理解这个问题的过程中,我试图创建一个效率较低的算法,将最坏情况下的编码效率限制在 0% 以上的任何地方,但失败了。似乎无论我尝试哪种编码,即使一半的存储保证是可写的,在一个天文数字上不太可能发生的事件:预先存在数据的排序可以确保我们甚至无法编码新数据的一点。这并不是实际问题陈述的真正问题,因为我关心的是平均案例效率,但这令人不安。
答:
我怀疑预期容量接近限制的 50%,因为 位数 n → ∞。
我想到的编码算法使用线性代数 有限域 F2.提前选择 ε > 0 和随机 维数为 (1 − ε) n/2 × n 的矩阵 A。解码向量 x,如果 x 不是 所有 1,则返回矩阵向量乘积 A (1 − x);否则 失败。要对向量 b 进行编码,请使用高斯消元法求解 A′ (1 − x′) = b 中的非零 x′,其中 A′ 省略对应的列 到预先存在的一位,x′ 省略对应于 预先存在的一位。如果没有解决方案,就打花边 卡。
我没有时间写和验证正式证明,但我的直觉 是,在获取 A 的子矩阵时,我们遇到的概率 线性依赖性随着ε边远离零而迅速降低。
我实现了该算法的更实用版本的模拟 在下面的 C++ 中(可以扩展到编码器/解码器对,而无需 太麻烦了)。它不是全有或全无,而是决定了 它可以控制的最长前缀,并将块用作 6 位长度 其次是该数据量,实现了原始存储的 ≈38%。 长度前缀在这里吃掉了大约 9%(我们控制了 ≈47% 的 bits),但在 large-n 限制中接近 0%。即使使用 64 位 块,你可以通过霍夫曼编码长度来做得更好。
(您可能想知道,如果我们甚至无法控制所有 长度位。我们可以进行设置,以便花边卡解码为 长度 0,这意味着它被跳过,然后打一张花边卡 每当必须发生这种情况时。
#include <algorithm>
#include <cstddef>
#include <iostream>
#include <limits>
#include <optional>
#include <random>
#include <vector>
namespace {
static std::random_device g_dev;
class Vector {
public:
explicit Vector() = default;
static Vector Random() {
return Vector{std::uniform_int_distribution<Bits>{
0, std::numeric_limits<Bits>::max()}(g_dev)};
}
Vector &operator^=(const Vector v) {
bits_ ^= v.bits_;
return *this;
}
bool operator[](const std::size_t i) const { return bits_ & (Bits{1} << i); }
private:
using Bits = unsigned long long;
explicit Vector(const Bits bits) : bits_{bits} {}
Bits bits_ = 0;
};
class Matrix {
public:
static Matrix Random(const std::size_t num_rows) {
Matrix a;
a.rows_.reserve(num_rows);
for (std::size_t i = 0; i < num_rows; ++i) {
a.rows_.push_back(Vector::Random());
}
return a;
}
Matrix RandomSubsetOfRows(const double p) const {
Matrix a;
for (const Vector row : rows_) {
if (std::bernoulli_distribution{p}(g_dev)) {
a.rows_.push_back(row);
}
}
return a;
}
std::size_t NumControllablePrefixBits() && {
for (std::size_t j = 0; true; ++j) {
const auto pivot = [&]() -> std::optional<std::size_t> {
for (std::size_t i = j; i < rows_.size(); ++i) {
if (rows_[i][j]) {
return i;
}
}
return std::nullopt;
}();
if (!pivot) {
return j;
}
std::swap(rows_[j], rows_[*pivot]);
for (std::size_t i = 0; i < rows_.size(); ++i) {
if (i != j && rows_[i][j]) {
rows_[i] ^= rows_[j];
}
}
}
}
private:
std::vector<Vector> rows_;
};
} // namespace
int main() {
static constexpr std::size_t kBlocks = 10000;
static constexpr std::size_t kLogBlockSize = 6;
static constexpr std::size_t kBlockSize = 1 << kLogBlockSize;
std::size_t num_usable_bits = 0;
const Matrix a = Matrix::Random(kBlockSize);
for (int i = 0; i < kBlocks; ++i) {
num_usable_bits +=
a.RandomSubsetOfRows(0.5).NumControllablePrefixBits() - kLogBlockSize;
}
std::cout << static_cast<double>(num_usable_bits) / (kBlocks * kBlockSize)
<< "\n";
}
评论
A
我在 Python 中实现了完整的算法。我用单位矩阵来扩充矩阵,就像矩阵反演算法一样,但将对应于孔的列归零。
import math
import random
log_n = 10
n = log_n + ((1 << log_n) - 1)
matrix = [random.randrange(1 << n) for j in range(n)]
def decode(data):
text = 0
for j in range(n):
if (data & (1 << j)) == 0:
text ^= matrix[j]
return text
def invert(existing_data):
sub = [matrix[j] for j in range(n) if (existing_data & (1 << j)) == 0]
inverse = [1 << j for j in range(n) if (existing_data & (1 << j)) == 0]
i = 0
while True:
for k in range(i, len(sub)):
if sub[k] & (1 << i):
break
else:
return inverse[:i]
sub[i], sub[k] = sub[k], sub[i]
inverse[i], inverse[k] = inverse[k], inverse[i]
for k in range(len(sub)):
if k != i and (sub[k] & (1 << i)):
sub[k] ^= sub[i]
inverse[k] ^= inverse[i]
i += 1
def encode(inverse, text):
data = ~((~0) << n)
for i in range(len(inverse)):
if text & (1 << i):
data ^= inverse[i]
return data
def test():
existing_data = random.randrange(1 << n)
inverse = invert(existing_data)
payload_size = max(len(inverse) - log_n, 0)
payload = random.randrange(1 << payload_size)
text = payload_size ^ (payload << log_n)
data = encode(inverse, text)
assert (existing_data & ~data) == 0
decoded_text = decode(data)
decoded_payload_size = decoded_text & (~((~0) << log_n))
decoded_payload = (decoded_text >> log_n) & (~((~0) << payload_size))
assert payload_size == decoded_payload_size
assert payload == decoded_payload
return payload_size / n
print(sum(test() for i in range(100)))
上一个:作用域为调用堆栈的变量
下一个:编译器是否会优化调用一次的函数
评论