【2025蓝桥杯省赛填空压轴题-pythonA组和研究生组】Ipv6 解析(四维dp)
这是一个很有意思的题目,思路和方法可以非常多,做为一个5分的填空题实在可惜,目前看了一圈网上的解答,很少有完整解答的,这里给一条思路和代码,仅供参考。
文章目录
- A.题目
- B.思路
- 1.压缩
- 2.可能长度
- 3.四维dp
- 4.结果统计
- C.代码
- D.答案
A.题目
【问题描述】
小蓝最近在学习网络工程相关的知识。他最近学习到,IPv6 地址本质上是
一个 128 位的二进制数,而字符串形式的 IPv6 地址是由被冒号分开的八段 16
进制数组成的, 例如,下面每行是一个字符串形式的 IPv6 地址:
0000:0000:0000:0000:0000:0000:0000:0000
0000:0001:0000:0000:0000:0001:0000:0000
0000:0001:00ab:0000:0023:0000:0a00:0e00
0000:0000:00ab:0000:000a:0001:0a00:0e00
0000:0000:00ab:0000:0000:0001:0a00:0e00
其中,每一段最长 4 位,且每一段的前导零都可以去掉(如果 4 位都为 0
需要写成 0)。
另外,IPv6 地址还可以将其中相邻的值为 0 的段合并压缩起来,用两个冒
号来表示,不过只能压缩一段。
例如上述地址最短的压缩后的形式分别为
::
0:1::1:0:0
0:1🆎:23:0:a00:e00
:🆎0🅰️1:a00:e00
0:0🆎:1:a00:e00
小蓝想知道, 所有 IPv6 地址的最短压缩形式的长度的和为多少?由于答案
很大(甚至超过了 128 位二进制整数的范围),请填写答案时填写这个总和除以
109 + 7 的余数。
【答案提交】
这是一道结果填空题,你只需要算出结果后提交即可。本题的结果为一个
整数(在 0 到 109 + 6 的范围内),在提交答案时只填写这个整数,填写多余的
内容将无法得分。
B.思路
1.压缩
第一个关键点在于理解Ipv6压缩的思想:【可以将最长的一段“连续的全 0 分组”用 :: 压缩一次】
例如把 0000:0000:00ab:0000:000a:0001:0a00:0e00 压成 :🆎0🅰️1:a00:e00 等。
注意:
- 如果没有长度 ≥2 的连续全零分组,则不值得使用 ::(对单个 0 分组用 :: 反而会变长)。
- 若存在多段长度相同的连续零分组,都能达到相同的压缩长度,随便选哪一段做压缩都不会影响最终的最短表示长度。
不压缩时的长度 = 所有分组去掉前导零后的长度之和 + 7( 7 个冒号)
如果有一段最大长度为
r
(
≥
2
)
r(\ge2)
r(≥2) 的“连续零分组”,那么使用
:
:
::
:: 可以把那
r
r
r 个零分组从
r
∗
‘
‘
0
"
+
(
r
−
1
)
∗
‘
‘
:
"
r * ``0" + (r-1) * ``:"
r∗‘‘0"+(r−1)∗‘‘:" 替换成
‘
‘
:
"
``:"
‘‘:"。
- 替换前此部分占 r + ( r − 1 ) = 2 r − 1 r + (r - 1) = 2r - 1 r+(r−1)=2r−1 个字符,替换后只剩下 : : :: :: 两个字符,故可少占 ( 2 r − 1 ) − 2 = 2 r − 3 (2r - 1) - 2 = 2r - 3 (2r−1)−2=2r−3 个字符
- 若最大连续零分组长度 < 2 < 2 <2,则不压缩。
2.可能长度
第二个关键点在于,可以把每个 16 位分组(0–65535)在去除前导零后,可能的“表示长度”及其出现次数统计出来:
分组值为 0:只会表示成 “0”,长度 =1。这种情况共有 1 种取值。
分组值非零时,根据最高有效的十六进制位在哪儿,可分为:
- 长度 1 (十六进制 1 位):有 15 种取值 (0x1 ~ 0xf)
- 长度 2:有 240 种取值 (0x10 ~ 0xff)
- 长度 3:有 3840 种取值 (0x100 ~ 0xfff)
- 长度 4:有 61440 种取值 (0x1000 ~ 0xffff)
3.四维dp
一条 IPv6 地址只有 8 段,我们可以在“分组层面”做一个多维 DP。
核心思想是:逐段扫描 8 个分组,用 DP 记录“目前已经处理到第几段、当前连续零分组长度、已出现的最大连续零分组长度、当前总的字符和”下,有多少种地址取值组合”。
我们令 dp [ p o s ] [ c u r R u n ] [ m a x R u n ] [ s u m L e n ] \text{dp}[pos][curRun][maxRun][sumLen] dp[pos][curRun][maxRun][sumLen],表示:当已经处理到第 p o s pos pos 段(取值范围 0~8),当前连续零分组长度是 c u r R u n curRun curRun,历史上最大的连续零分组长度是 m a x R u n maxRun maxRun,并且已经累加的“去前导零后字符总和”为 s u m L e n sumLen sumLen 时,有多少种具体的分组取值序列能到达这里。
四个维度的取值范围:
- p o s pos pos 范围是 0~8(共 9 个状态,用于表示“还没开始到处理完第 8 段”)
- c u r R u n curRun curRun 范围是 0~8(因为一口气最多可能 8 个分组都为 0)
- m a x R u n maxRun maxRun 范围也是 0~8
- s u m L e n sumLen sumLen 最大不会超过 8 × 4 = 32 8 \times 4 = 32 8×4=32(如果全部分组都是 4 位表示),最小是 8 8 8(如果全部分组都是 0,表示长度都为 1),所以可以开到 0~32 即可
状态转移:
在处理下一段 p o s → p o s + 1 pos \rightarrow pos+1 pos→pos+1 时,需要考虑这段可能是:
- 0,表示长度 =1,有 1 种取值
- 则新状态里 n e x t C u r R u n = c u r R u n + 1 nextCurRun = curRun + 1 nextCurRun=curRun+1
- n e x t M a x R u n = m a x ( m a x R u n , n e x t C u r R u n ) nextMaxRun = max(maxRun, nextCurRun) nextMaxRun=max(maxRun,nextCurRun)
- n e x t S u m L e n = s u m L e n + 1 nextSumLen = sumLen + 1 nextSumLen=sumLen+1
- 非零分组,表示长度可能是 1、2、3、4;相应取值种数分别是 15、240、3840、61440
- 则新状态里
n
e
x
t
C
u
r
R
u
n
=
0
nextCurRun = 0
nextCurRun=0 (因为连续零被断开了)
• n e x t M a x R u n = m a x R u n nextMaxRun = maxRun nextMaxRun=maxRun (不变)
• n e x t S u m L e n = s u m L e n + L nextSumLen = sumLen + L nextSumLen=sumLen+L (L 为该分组的表示长度 1/2/3/4)
• 每种转移要把路径数 dp [ p o s ] [ c u r R u n ] [ m a x R u n ] [ s u m L e n ] \text{dp}[pos][curRun][maxRun][sumLen] dp[pos][curRun][maxRun][sumLen] 乘以“这一段对应的取值种数”再累加到下一状态去。
- 则新状态里
n
e
x
t
C
u
r
R
u
n
=
0
nextCurRun = 0
nextCurRun=0 (因为连续零被断开了)
4.结果统计
当 p o s = 8 pos = 8 pos=8 时,说明 8 段都处理完了, s u m L e n sumLen sumLen 就是这 8 段去掉前导零后的总长度, m a x R u n maxRun maxRun 是全局最长连续零分组长度。
- 对每个末状态
dp
[
8
]
[
c
u
r
R
u
n
]
[
m
a
x
R
u
n
]
[
s
u
m
L
e
n
]
\text{dp}[8][curRun][maxRun][sumLen]
dp[8][curRun][maxRun][sumLen],其最短表示长度就是
(sumLen) + 7 − max ( 0 , 2 × ( maxRun ) − 3 ) \text{(sumLen)} + 7 \;-\; \max\bigl(0,\;2\times(\text{maxRun}) - 3\bigr) (sumLen)+7−max(0,2×(maxRun)−3) - 若 m a x R u n < 2 maxRun < 2 maxRun<2,则减数为 0 (不使用 : : :: ::)
- 若 m a x R u n ≥ 2 maxRun \ge 2 maxRun≥2,则减数为 ( 2 × maxRun − 3 ) (2\,\times\,\text{maxRun} - 3) (2×maxRun−3)
- 把这一“最短表示长度”乘上末状态对应的路径数量(也就是有多少具体 IPv6 地址对应到这个状态),然后再对所有末状态求和,即可得到所有 IPv6 地址的最短表示长度之和。最后取模 ( 1 0 9 + 7 10^9+7 109+7) 输出。
C.代码
import java.util.Arrays;
public class Main {
static final int MOD = (int)1e9 + 7;
public static void main(String[] args) {
// 按照表示长度 1/2/3/4 分别是多少种取值
int[] nonZeroLen = {1, 2, 3, 4};
// 长度=1(非零): 15 种; 长度=2: 240; 长度=3: 3840; 长度=4: 61440
int[] nonZeroWays = {15, 240, 3840, 61440};
/**
* dp[pos][curRun][maxRun][sumLen]:表示前 pos 段处理完后,连续零分组数=curRun,
* 历史最大连续零分组数=maxRun,已累计分组字符和=sumLen 的情况下,共有多少种 IPv6 地址
* 把第4维sumLen合并到三维数组里,减少一维
*/
long[][][] dpCur = new long[9][9][33];
long[][][] dpNxt = new long[9][9][33];
dpCur[0][0][0] = 1;
// 逐段处理
for (int pos = 0; pos < 8; pos++) {
// 清空下一层
for (int curRun = 0; curRun <= 8; curRun++) {
for(int k = 0; k <= 8; k++) {
Arrays.fill(dpNxt[curRun][k], 0);
}
}
// 枚举状态
for (int curRun = 0; curRun <= 8; curRun++) {
for (int maxRun = 0; maxRun <= 8; maxRun++) {
for (int sumLen = 0; sumLen <= 32; sumLen++) {
long count = dpCur[curRun][maxRun][sumLen];
if (count == 0) {
continue;
}
// 1) 零分组 => 表示长度=1
int nextCurRun0 = curRun + 1; // 连续零分组 + 1
int nextMaxRun0 = Math.max(maxRun, nextCurRun0);
int nextSumLen0 = sumLen + 1;
if (nextSumLen0 <= 32) {
dpNxt[nextCurRun0][nextMaxRun0][nextSumLen0] =
(dpNxt[nextCurRun0][nextMaxRun0][nextSumLen0] + count) % MOD;
}
// 2) 非零分组 => 可能长度=1/2/3/4
for (int i = 0; i < nonZeroLen.length; i++) {
int L = nonZeroLen[i];
int waysThis = nonZeroWays[i];
int nextCurRun = 0; // 连续零断掉
int nextSumLen = sumLen + L;
if (nextSumLen <= 32) {
long ways = (count * waysThis) % MOD;
dpNxt[nextCurRun][maxRun][nextSumLen] =
(dpNxt[nextCurRun][maxRun][nextSumLen] + ways) % MOD;
}
}
}
}
}
// dpNxt -> dpCur
long[][][] tmp = dpCur;
dpCur = dpNxt;
dpNxt = tmp;
}
// 处理完 8 段后,dpCur[curRun][maxRun][sumLen] 就是所有地址在“分组层面”统计的结果
// 最后把最短表示长度 累加到答案
long answer = 0;
for (int curRun = 0; curRun <= 8; curRun++) {
for (int maxRun = 0; maxRun <= 8; maxRun++) {
for (int sumLen = 0; sumLen <= 32; sumLen++) {
long count = dpCur[curRun][maxRun][sumLen];
if (count == 0) {
continue;
}
// 不压缩长度 = sumLen + 7
// 压缩贡献 = 如果 maxRun >= 2,则可减 (2*maxRun - 3)
int reduce = 0;
if (maxRun >= 2) {
reduce = 2 * maxRun - 3;
}
int minLen = (sumLen + 7) - reduce;
// 贡献 = minLen * count
long contrib = (long)minLen * count;
answer = (answer + (contrib % MOD)) % MOD;
}
}
}
// 983499503
System.out.println(answer);
}
}
D.答案
983499503
ATFWUS 2025-04-14