洛谷-P8162 让我们赢得选举 题解

Solution

\(N\le 500\),我们需要 \(O(N^3)\) 的算法。

直接状压 dp 至少 \(O(2^N)\),我们一定有大量最优解的性质没有找到。

为了方便,约定一个州有三种状态:

  • B 类点:演讲至 \(B_i\) 小时,同时获得选票和协作者。
  • A 类点:演讲至 \(A_i\) 小时,只获得选票。
  • 无效点:不去,演讲时间为 \(0\)。

对于 \(B_i=-1\) 的州,不妨令 \(B_i=+\infty\)。


我们来找一些最优解的性质:

性质 1

不会出现并行演讲的情况,任意时刻,所有人一定在一起演讲。

设目前共有 \(p\) 人,则这一时刻演讲速度恒为 \(p\)。相比并行演讲,我们完全可以让这 \(p\) 个人集中到同一个尚未完成的州中。这样不仅不会增加完成所有目标所需的时间,而且如果集中演讲刚好获得了新的协作者,之后的演讲速度还会更快。

因此存在最优方案,使得在每个时刻所有人都在同一个州演讲。

性质 2

不会出现 A 类点排在 B 类点前面的情况。

临项交换论证即可。

性质 3

B 类点一定按照 \(B\) 从小到大的顺序做。

证明:考虑临项交换,不妨设 \(i,j\) 相邻,此前共有 \(a\) 个人。

  • 先做 \(i\):\(\frac{B_i}{a+1}+\frac{B_j}{a+2}\)

  • 先做 \(j\):\(\frac{B_j}{a+1}+\frac{B_i}{a+2}\)

上减下得:

\\\left(B_i-B_j\\right)\\left(\\frac{1}{a+1}-\\frac{1}{a+2}\\right)\<0 \\

性质 4

如果 \(i\) 是 B 类点,那么所有满足 \(B_j<B_i\) 的点 \(j\) 都一定不是无效点。

调整法证明。如果出现上述情况,那么令 \(j\) 变成 B 类点,\(i\) 变成无效点一定更优。


这些性质够了。\(O(N^3)\) 的复杂度启发我们 dp。我们需要:

  • 钦定 \(k\) 个 B 类点,按 \(B\) 值从小到大做
  • 再从剩下的州里选 \(m-k\) 个 \(A\) 值最小的作为 A 类点做

我们考虑先把州按 \(B\) 从小到大排序。发现 \(k\) 对统计 A 类点的贡献系数有很大影响,我们考虑外层枚举 \(k\)。

此时不难得到一个 \(O(N^4)\) 的算法:设 \(f_{i,a,b}\) 表示前 \(i\) 个州选了 \(a\) 个 A 类点,\(b\) 个 B 类点的最小时间。

结合性质 4,我们发现最后一个 \(B\) 类点前面一定没有无效点。因此我们枚举最后一个 \(B\) 类点的位置并把序列拆成前后两部分。

前半部分满足 \(a+b=i\),状态数减少。即 \(f_{i,b}\) 表示前 \(i\) 个点全部选入,有 \(b\) 个 B 类点的最小时间。\(f\) 主要是在决定前缀中的哪些州是 B、哪些是 A,真实时间顺序由性质 2、3 自动确定。

后半部分也可以 \(O(n^2)\) 预处理,详见代码中 g 数组。

此时总时间复杂度已经优化到了 \(O(N^3)\),可以通过。

Code

cpp 复制代码
#include <bits/stdc++.h>
#define rep(i,a,b) for(int i(a);i<b;++i)
#define per(i,a,b) for(int i(a);i>b;--i)
#define rept(i,a,b) for(int i(a);i<=b;++i)
#define pert(i,a,b) for(int i(a);i>=b;--i)
#define fi first
#define se second
#define db double
#define me(x,y) memset(x,y,sizeof(x))
using namespace std;
using Elem=pair<db,db>;
constexpr int N=502;
constexpr db INF=1e32;
Elem x[N];
db f[N][N],g[N][N],ans;
int n,m;
inline void upd(db &x,db y){x=min(x,y);}
// f[i][b]: 前i个点全部选入,有b个B类点的最小时间
// g[i][j]: 在i~n中选j个A类点的最小值
signed main(){
    cin.tie(0)->sync_with_stdio(0);
    cin>>n>>m;
    rept(i,1,n){
        cin>>x[i].fi>>x[i].se;
        if(x[i].se<0) x[i].se=INF;
    }
    sort(x+1,x+n+1,[](const Elem &x,const Elem &y){return x.se<y.se;});
    me(g,100);  // 统一赋值成一个很大的数
    g[n+1][0]=0;
    pert(i,n,1){
        rept(j,0,n-i+1){
            if(j) upd(g[i][j],g[i+1][j-1]+x[i].fi);
            upd(g[i][j],g[i+1][j]);
        }
    }
    ans=g[1][m];
    rept(k,1,m){
        me(f,100),f[0][0]=0;
        rep(i,0,n) rept(b,0,min(i,k)){
            upd(f[i+1][b+1],f[i][b]+x[i+1].se/(b+1));  // B类点
            upd(f[i+1][b],f[i][b]+x[i+1].fi/(k+1));  // 这个A类点实际在最后做,那时已经有k+1个人。
        }
        rept(i,k,m) upd(ans,f[i][k]+g[i+1][m-i]/(k+1));  // 枚举最后一个b类点的位置,更新答案
    }
    cout<<format("{:.15f}",ans);
    return 0;
}