• [技术干货] 第39期 题解思路与源码:固定时隙均衡算法(SlotWeave)
    固定时隙均衡算法:从增量放置到保留重叠的重建源码仓库:XJU-ICTHUB/SlotWeave代码入口:src/solution.cpp算法说明:固定时隙均衡算法个人题解合集:华为算法挑战赛题解与实现前言第39期也结束了,把这次固定时隙均衡算法的思路整理一下。上期整理的是第38期 MemWeave ,加上这期源码和题解已统一了一下,整理到我们实验室组织仓库了。“华为算法挑战赛题解与实现”,按期数索引;后面继续增加新的期数,查起来方便一些。不过之后也不一定有,要是去实习被招安那应该就打不了,不过到时候可以给大家整几个baseline让大家参考一下。依旧前期霸榜,后期被超,麻了。以下就是AI整理了,欢迎大家交流。这篇讲的是从参赛工程中整理出的完整通用方案:链式DP、多层重建、局部搜索、初始化优化和极小规模搜索。预计官方分数应该在0.849以上。这道题的难点不只在“把点放得均匀”:每步可以改变数量,也可以移动其他服务,但每多动一个格子都要付稳定性成本。因此,方案的核心是把均匀度和额外改动放进同一个代价模型,再让不同规模的搜索共享这套模型。1. 问题与两个质量目标圆环有 N 个位置,下标为 0..N-1。位置中的非零整数是服务 ID,0 表示空位;空位也作为一种类型参与均匀度评价。增加服务 x 的 K 个颗粒,至少要将 K 个空位改成 x;删除则相反。允许移动其他颗粒,但会影响稳定性。每步质量部分是J = 0.55U + 0.35V.其中 U 是布局均匀度,V 是稳定性。官方还包含速度分,因此质量更高但工作量过大的算法,综合结果可能更差。本题是函数实现题,N ≤ 10000,操作数 Q ≤ 1000。评测器负责输入输出,求解器只实现四个接口:void init(int N, const std::vector<std::pair<int,int>>& services);void apply_add(int id, int count);void apply_remove(int id, int count);const std::vector<int>& grid();每次操作只能使用当前状态与已经发生的信息,事先不知道后续操作。grid() 返回当前数组的引用,不复制整幅布局。2. 将均匀度转成整数 gap 代价类型 t 有 s 个颗粒。沿圆环顺序,记相邻同类型颗粒之间的正向距离为 g;最后一个到第一个的距离也要计算,所有 gap 的和为 N。只有一个颗粒时,它到自身的一周距离是 N,不能写成 0。题面的偏差为Dt = ∑ g ∣ g − (N) / (s) ∣ .引入整数偏差Et = ∑ g∣sg − N∣ = sDt.当 2≤s<N 时,类型均匀度可以写成ut = 1 − (Et) / (2 ( s − 1 ) ( N − s ) ).再乘该类型占整个网格的比例 s/N,得到1 − U = (1) / (2N) ∑ t :  2 ≤ st < N(stEt) / ( ( st − 1 ) ( N − st ) ).数量为 0、1 或 N 的类型需要单独处理:空类型没有权重,后两种情况按题面得满分,内部代价系数置零。实现使用整数权重和总代价Ft ≈ (256stN) / ( ( st − 1 ) ( N − st ) ) , P = ∑ tFtEt ≈ 512N2 ( 1 − U ) .setType 构造类型权重;cst 计算裸 gap 代价,wcst 计算加权 gap 代价;totalLayoutPenalty 求整个布局的内部损失。若 N=qs+r,最均衡的间隔是 s−r 个 q 和 r 个 q+1,对应Etmin = 2r ( s − r ) .例如 N=10、s=3,间隔 3、3、4 已经最均衡,该类型均匀度仍只有 6/7。图1 只画类型x的三个颗粒,其余格子可由其他服务或空位占据。圆环上的三个gap为3、3、4,总和为10。3. 稳定性与每个额外格子的价格令 K 为本次实际成功增删的数量,M 为最终与本次操作开始前不同的位置数。N>K 时V = 1 − (M − K) / (N − K).所以先只修改 K 个必要位置,就有 M=K、V=1。N=K 时按题面直接取 V=1,避免除以零。后续交换不改变类型数量,却可能增加或减少 M。对格子 i,内部保存本次首次修改前的类型;再次写入时与这个原类型比较。把格子改回原类型能够退还稳定性成本。交换 p、q,当前类型为 a、b,操作入口类型为 O 时,改动数差值为ΔM = [ b ≠ Op ] − [ a ≠ Op ] + [ a ≠ Oq ] − [ b ≠ Oq ] .代码通过 markChange、changedNow、changedIf 和 stabD 维护这套记账。时间戳避免每次清空全部辅助数组;只有属于当前操作的记录才会被读取。为什么使用前瞻 H一次额外搬动立刻付稳定性成本,但更均匀的状态可能延续若干步。算法用 H 表示预期受益时长,将一个候选的内部价值写成ΔP + λHΔM , λH ≈ (7 ⋅ 512N2) / (11H ( N − K ) ).负值表示改善这个模型。H 越大,算法越愿意为布局质量支付搬动成本。currentHorizon 根据已处理的调用数逐渐增加前瞻,再施加上限;不同搜索阶段还会使用各自的前瞻上限和预算。4. 让复杂搜索可运行的增量索引外部服务 ID 不要求连续。代码用映射将其压缩成内部类型编号,维护以下状态:当前网格、每种类型的数量,以及外部 ID 与内部编号的对应关系。非空位类型的环形双向链,快速获得一个颗粒的同类型前后邻居。空位位图及非空字摘要,快速查询附近空位、前后空位和窗口候选。类型位图,支持按位置顺序批量收集某类颗粒。类型代价表、总损失缓存,以及标记哪些类型需要重算的 dirty 状态。当前操作的修改记账、局搜种子队列与工作预算。移动一个颗粒只影响其移除和插入处附近的 gap。总损失的计算也不必每次重新遍历所有类型;未变化类型可以复用缓存。5. 强制增删:同时照顾服务与空位先看最简单的局部差值。在一个类型的相邻颗粒之间插入 p,把一个 gap a+b 拆成 a、b。定义 f_s(g)=|sg−N|,则ΔEinsert = fs ( a ) + fs ( b ) − fs ( a + b ) .删除的变化恰好相反。插入空集合的第一个颗粒时增加 f_s(N);删除最后一个颗粒时减去 f_s(N)。增加 x 的一个格子,会同时向 x 插入一点、从空位集合删除一点。因此选点代价包含服务与空位两部分,不能只把 x 放进自己的最长 gap 而完全忽略剩余空位。placeAddedGranules 和 selectRemovedGranules 是强制阶段入口。基础路径在目标位置附近的有限窗口查找合法颗粒,比较完整的局部模型;批量较大时则进入下面的动态规划。批量操作必须固定最终计数一次增加 K 个颗粒时,先确定最终的 s'=s+K 和 e'=e−K。K 次选点中始终使用这两个数量构造 gap 目标与权重。如果每选一个点就按中间数量重新定义理想 gap,其余所有间隔的代价也会改变,仅计算插入点附近的三项差值便不再完整。固定最终计数后,中间状态只是搜索势函数,最终状态才对应本次操作的目标。填满全部空位、删除整个类型等情况有直接的批处理路径;一般情况下,强制阶段只在来源类型与目标类型之间改色,恰好改变 K 个不同格子。6. 批量增加的配额分配与链式 DP批量增加不能简单地重复 K 次互不关联的最近空位选择:多个新点可能共享同一个旧 gap,前面的选择会影响后面的间隔。placement_dp 将它分成“分配数量”和“协同选位置”两层。6.1 先决定每个旧 gap 放多少点已有类型 x 的旧颗粒固定。旧 gap 长度为 g,放入 k 个新颗粒后得到 k+1 段;理想段数约为 s'g/N。初始取 k≈floor(s'g/N)−1,并夹到该 gap 的空位容量范围。取整后总数可能不等于 K,于是按余数或欠额继续补回、扣回名额,直到满足总数。这是带容量限制的 Hamilton 配额思想。实现还会用空位供给调整配额倾向,避免某段长度很大却几乎没有合法空位。入口为 placement_dp::addExisting。新类型没有固定旧颗粒,addNewType 会尝试少量相位和锚点,将圆环展开成首尾相接的一条链。6.2 状态、转移与回溯考虑固定端点 S、T 之间插入 k 个点。在 k 个近似等距目标附近各取一个候选带,每条选点链保持真实位置递增;不同层的候选带可以重叠。记第j层的候选集合为 Cj,pu 是候选u的真实圆环展开位置。状态 DPj ( u ) 表示前j个新颗粒已经确定、最后一个选在候选u的最小模型代价:DPj ( u ) = n ( u ) + min ⁡ v ∈ Cj − 1;pv < pu { DPj − 1 ( v ) + fx ( pu − pv ) + c ( v , u ) } .这里 f_x 是服务 gap 的加权边代价,n(u) 是填掉该空位的节点代价,c(v,u) 修正两个相邻旧空位同时被填掉时的相互影响。首尾边连接固定端点;保存最优前驱后统一回溯所有位置。相关函数是 buildTargets、solveSeg、chainW 和 denseChain。选点完成后批量写入并重连类型链,避免反复维护大量临时链结构。空位节点使用一个折线函数表示。若最终空位数为 e',定义ρ ( g ) = 2C0max ⁡ ( N − e ′ g , 0 ) .去掉一个两侧 gap 为 a、b 的旧空位,其形状相关项为 ρ(a+b)−ρ(a)−ρ(b)。绝对值展开中的线性项,在固定 K 的方案比较中是常数,可以略去。代码中的 rh0、nodeCost 与 pairCost 对应这些项。服务侧链间隔可以直接准确累计;空位侧采用节点和相邻对交互模型。6.3 利用 V 形边权加速服务边权形如 |s'(p_u−p_v)−N|。将位置线性变换为 Y(p) 后,转移可按绝对值折点分成两段,维护前缀最小值和单调队列,避免遍历所有前驱。一般候选带宽 W 的朴素 DP 为 O(kW²);适用该优化的链路径约为 O(kW)。窄带版本则直接展开固定宽度转移,用较小常数替代复杂维护。相邻旧空位的特殊修正单独处理。7. 批量删除:选择留下来的晶格删除 K 个 x 等价于从原有 s 个位置中选择 s−K 个留下。这种表述可以直接控制删除后服务的完整间隔。placement_dp::removeLattice 围绕剩余颗粒的近似均匀目标,在原颗粒列表上取候选带;状态表示已经选择若干保留点及最后一个候选。相邻保留点的距离提供服务边权,它们之间未保留的位置变成空位,相关项通过节点或前缀量累计。最后统一释放所有未被选中的位置。latticeDP 及其窄带特化共享这条保序选择主线。8. 强制阶段之后的稳定性定价搜索8.1 快速种子局搜runLocalSearch 从本次改点及同类型邻居出发,尝试将颗粒移向合并 gap 的中点附近。移动通过交换实现,候选价值包含双方类型的 gap 变化和 λ_HΔM。接受后,把同类型邻接关系发生变化的格子重新放入种子队列。这是一种工作量较小的局部修复,不需要遍历全部位置对。8.2 MN:更强的伙伴搜索mnRun、mnDescent 和 mnBestX 扩大候选范围。源点来自本次改点、操作类型颗粒及其他有潜力位置;伙伴包括近邻窗口、合并 gap 中点、大 hole 中点,以及能恢复本次入口原类型的位置。先用“本类型最好可能移动 + 稳定性价格”的下界过滤,再对有希望的源点寻找最好伙伴。每次接受后增量更新邻居缓存、hole 列表与队列。下界剪枝能省去不可能改善当前模型的评估。每操作信用桶、评估上限和交换上限控制工作量。sqPre 在重建检查前先做一轮较广的搜索:如果能便宜地消除局部缺陷,就可能省掉一次昂贵重建。8.3 保序重排与空位修复relat 把一种类型的颗粒按圆环顺序编号,每个颗粒在窄位移带中选新位置。DP 用 gap 边权连接相邻新位置,节点包含交换伙伴代价与稳定性价格,同时约束顺序不交叉。vcRelat0 将活跃空位限制在固定空位之间的连续段上;relx4 在“服务 x + 空位”的有序列表中重新选择哪些位置属于 x。这些方法比独立交换更能协调一段布局。删除后的回填与新增位置的重新选择,则利用同一步内已经付过的改动成本。它们遵守同一原则:改动价格由操作入口与最终布局的差异决定。9. OM:保留旧位置的分层重建局搜可能困在局部极值,多类型也可能同时碎片化。全局重建能带来新的排列,但直接套一个均匀模板会破坏大量旧位置。OM 的目标是同时得到较好的分层布局,并尽量保留有价值的重叠。入口是 buildOverlapReconstruction。9.1 从固定 DDA 轨迹变成带宽 DP按类型规模顺序处理,每层从前面类型尚未拿走的位置中选 s 个。剩余列表长度为 L 时,理想秩间隔约为 floor(L/s) 或再加一。普通 DDA 固定这些间隔的先后次序。OM 则围绕理想秩轨迹维护小幅漂移状态,由 DP 选择次序;也可以允许略小或略大的间隔,并付偏差代价。节点奖励“选到这个类型原本占据的位置”,并惩罚抢走其他已经放得较好类型的格子。命中奖励由稳定性价格导出,局部良好标记由相邻 gap 判断。9.2 dense 与 sparse 两种层dense 层在较紧凑的剩余列表上选择,主要使用秩间隔偏差,并及时压缩列表。sparse 层使用带 blocked 标记的共享列表,必要时再压缩,边权更多使用真实圆环距离。omLayerDense、omLayerLazy 和 omDP/omDPS* 实现这些分支。固定带宽的展开版本只是同类递推的加速形式。9.3 keep-DP 与 good 标记释放omKeepDP 给旧布局另一个机会:以旧位置为中心,只开放被前层阻挡部分附近的位移,其余位置尽量保留,再与新晶格候选比较。一个类型完成本层分配后,其未使用旧位置不应继续依靠 good 标记妨碍后续类型。因此要释放相关保护。还可以尝试少量不同的命中奖励,按统一净值选择较好的重建候选。9.4 触发与接受是两件事触发决定是否值得花时间生成候选。算法根据当前 P、各类型独立最均衡的下界、上次修复后残留的缺陷,估计布局恶化程度;再配合最小检查间隔、大操作条件和工作信用控制成本。minimumPossiblePenalty 是独立类型下界之和。候选生成后,重新累计内部损失和真实改动数增量。普通重建路径首先要求内部损失确实下降,即 Pcurrent > Pcandidate,再检查稳定性净值。典型接受条件为11HA ( N − K ) ( Pcurrent − Pcandidate ) > 7 ⋅ 512N2ΔM.这里 ΔM 是候选相对当前方案的 M 增量,但 M 始终相对本次操作入口计算。通过后由 applyReconstruction 落盘。10. 大操作后的链修正与移动成本返还rbxFar 从删除释放的格子寻找更远的合适回填伙伴;vpmLmp 和 hcRevertLS 重点查找能恢复入口类型、返还移动成本的交换。它们补充了只看近邻几格容易漏掉的机会。大型批量增加后,若一串新增 x 颗粒都受可用空位限制而偏离理想位置,zchRuns 将旧 x 颗粒作为锚点,把相邻新增点分成若干 run。每个新增点可以在窄位移带内与其他类型或空位交换;run-DP 用相邻新位置的 x gap 作边权,用伙伴代价与稳定性变化作节点,约束整条链不交叉,再回溯执行。zchPass 提供较轻的逐点修正路径。11. 初始化:多构造、IQ 搜索与保留最好候选初始化没有移动惩罚,也不在官方头文件说明的 apply/grid 计时范围内。buildInitialLayout 先按类型数量分层,在当前剩余位置列表上用整数 DDA 精确分配数量。每层累计器加 s,达到或超过列表长度 L 就选中并减 L;一遍扫描恰好选择 s 个位置。随后尝试无命中偏好的 OM 构造及少量层顺序变体,保留损失较低者,再做纯均匀度局搜。IQ 阶段包括两种互补搜索:polishInitialLayout:队列局搜,尝试局部窗口和同类型合并 gap 的中点,接受后重新排队受影响邻居。annealInitialLayout:阈值接受搜索,允许小幅上坡以越过局部极值,逐渐降低阈值,之后再做局搜并保存最好布局。随机化只负责生成普通搜索候选;随机流在初始化时重置,以便复现实验。12. 极小规模:完整模型与有预算的分支定界普通引擎已经包含较小圆环的广范围交换搜索。极小 N 则由独立的 tiny_search 引擎处理,使用更高精度的定点目标和更完整的搜索。每次先完成数量正确的强制修改,再执行全位置对局搜,最后进行分支定界。搜索逐格指定类型,同时满足最终数量。下界来自两个方向:剩余长度均分给剩余同类型 gap,得到凸代价的乐观下界;剩余类型配额无法复用全部旧位置时,加入不可避免的改动数。已经得到的好布局作为 incumbent,帮助尽早剪枝。13. 一次操作的完整阅读路线图2 每步先完成数量正确的必要修改,再在工作预算内修复和重建。processOperation 的主线可以概括为:确定实际增删量、阶段预算、前瞻价格与本次修改记账。完成强制增删,必要时用批量 DP 协调位置。执行有预算的种子局搜、类型或空位重排及大操作修正。在重建检查前做适用的预搜索,估计是否需要 OM。若值得,生成并评估保留重叠的重建候选,接受后落盘。做适用的退款修复、MN 搜索与收尾,恢复临时权重。具体模块会按规模、操作大小和预算选择性执行。configureWorkBudgets 集中承担这种成本取舍。14. 复杂度强制链 DP:长度 k、候选带宽 W,朴素转移 O(kW²),适用 V 形优化的路径约 O(kW),另有扫描与配额分配。保序重排:长度 s、位移带宽 O(w),一般为 O(sw²);活跃段版本用活跃长度替代 s。OM:多层窄带 DP,加上剩余列表维护、压缩和最终候选评估。局搜:每次只评估少量 gap,但总次数受候选窗口、信用桶与工作上限控制。极小图分支定界:最坏指数级,依靠规模门限、下界和节点预算封顶。题面限制乘除、取余、浮点和数学库。实现使用移位加法、二进制长除法、整数表和比较;环形下标用加减 N 归一化。固定宽度展开、位图扫描、增量缓存和容器访问优化降低了这些操作的常数。15. 代码组织与建议阅读顺序公开版按18个实现片段组织,仍由一个翻译单元按固定顺序包含。这样保留了原有内联、状态、模板和宏生命周期;片段是阅读边界,不应分别编译。想了解的部分对应文件与入口官方接口和引擎分派src/solution.cpp、src/detail/16_dispatch.inc一次操作的完整流程src/detail/11_operation_pipeline.inc:processOperation环形链、代价与修改记账src/detail/01_state_and_costs.inc批量增加、保留点删除DPsrc/detail/06_chain_dp.inc:placement_dp交换下降及较强伙伴搜索src/detail/03_swap_descent.inc、04_gap_relocation.incOM分层构造与keep-DPsrc/detail/08_rebuild_layers.inc重建触发与评估src/detail/09_rebuild_policy.inc,并结合操作流水线退款、回填与大操作修复src/detail/10_repair_and_polish.inc多构造及IQ初始化搜索src/detail/12_initial_search.inc极小规模搜索src/detail/15_tiny_engine.inc:tiny_search建议先看processOperation,把预算分支当作开关,先理解状态经过哪些阶段,再去读链DP和重建。第一遍不必从配置文件的数百个常量开始读。16. 编译、运行支持GCC/Clang,使用C++11;验证脚本需要Python 3。在仓库根目录执行:sh tools/build.sh./build/timeslot_interface_testspython3 tools/check_solution.pypython3 tools/verify.py --runner build/timeslot_runner./build/timeslot_runner tests/data/example1.txt./build/timeslot_runner tests/data/example2.txt --trace可以用CXX=g++-16 sh tools/build.sh或CXX=clang++ sh tools/build.sh指定编译器。内核使用编译器位查询内建函数。需要单文件时执行:python3 tools/amalgamate.py --out build/Solution.cpp生成的文件以#include "solution.h"开头,实现官方四函数,没有main()或输入输出。runner.cpp是独立的本地验证器,其浮点评分、计时和文件读写不属于提交内核。