P8357

· · 题解

理解题意后,不难发现一个事实:可以把硬币分成两堆称,然后合并起来。容易证明这是无后效性的。

所以就能根据题意推出方程。设 f_i 表示 i 个硬币中找假币的最小代价,有:

f_x=\operatorname{min}_{i=1}^{x-1}\{f_i+ai+b\lceil\frac{x}{i}\rceil\}

根据这个式子算就好了。但是这样 1D/1D 的转移是 O(n^2) 的,过不去。优化?单调队列似乎不适用于这题。

正解是找性质:发现转移的时候出现 b\lceil \frac{x}{i} \rceil,熟悉整除分块套路的都知道可以对 x-1 做整除分块。然后就显然了:对于每一块,取最左端点肯定是最优的。所以转移的时候只要转移每一块的左端点即可。

$$v(x)=\begin{cases} x & x\leq\lfloor\frac{n-1}{x}\rfloor\\ k+\lfloor\frac{n-1}{x}\rfloor & x>\lfloor\frac{n-1}{x}\rfloor \end{cases}$$ $k$ 是一个常数,取值范围大约在 $[\sqrt n,\inf)$,官方取的是 $10^5$。 有没有更优美的做法?其实有:C++ pbds 里有一个比 `unordered_map` 效率更高的容器 `hash_table`,用哈希值存储元素,平均下是 $O(1)$ 的。使用方式: - 头文件(有两个): ```cpp #include <ext/pb_ds/tree_policy.hpp> #include <ext/pb_ds/assoc_container.hpp> ``` - 命名空间:`using namespace __gnu_pbds;`。 - 声明方式:有两种哈希表: ```cpp cc_hash_table<int,int> cc;//链表 gp_hash_table<int,int> gp;//开放寻址 ``` 一般开放寻址会快一点,效率瓶颈主要是在处理哈希冲突的方式上。虽然如此,由于这题的特殊性质,使用链表哈希表会更快一些(最慢测试点比开放寻址法快了约 500ms)。 然后就可以用记忆化搜索通过本题了。略微卡常后最慢的点能跑进 $0.8$ 秒,也算是稳定的了。 ```cpp #include<bits/stdc++.h> #include<ext/pb_ds/tree_policy.hpp> #include<ext/pb_ds/assoc_container.hpp> using namespace __gnu_pbds; using namespace std; typedef long long ll; // 省略快读模板 using namespace fastio; ll n,a,b; cc_hash_table<ll,ll> gp;//关键 cc_hash_table<ll,ll> pre;//关键 ll dp(ll x){ auto at=gp.find(x); if(at!=gp.end())return at->second; ll ans=0x7fffffffffffffffll; ll N=x-1,p; for(ll L=1,R;L<=N;L=R+1){//整除分块 R=N/(N/L); ll cur=dp(L)+(N/L+1)*b+x*a; if(cur<ans)ans=cur,p=L; } pre[x]=p; return gp[x]=ans; } const int maxn=10000000+5; ll path[maxn],loc; int main(){ cin>>n>>a>>b; gp[1]=0,dp(n); int cur=n,v; while(v=pre[cur])path[++loc]=v,cur=v; wr(gp[n],' '),wr(loc);//wr 是快读输出函数 for(int i=1;i<=loc;i++)wr(path[i],' '); io::flush();//快读刷新缓冲区 return 0; } ```