题目
题目描述
众所周知,
H
a
n
d
I
n
D
e
v
i
l
\sf HandInDevil
HandInDevil 是又强又假又短的神仙。也兼任马桶一职。
现在 H a n d I n D e v i l \sf HandInDevil HandInDevil 有 n n n 句假话要说。正如标杆已经指出的那样, H a n d I n D e v i l \sf HandInDevil HandInDevil 说一句话,会付出代价。对于第 i i i 句假话, H a n d I n D e v i l \sf HandInDevil HandInDevil 有 k i k_i ki 个备选选项,其中第 j ( 1 ⩽ j ⩽ k i ) j\;(1\leqslant j\leqslant k_i) j(1⩽j⩽ki) 句假话会让 H a n d I n D e v i l \sf HandInDevil HandInDevil 失去 j j j 个 [最高机密],但是会让祂获得 a i , j a_{i,j} ai,j 的愉悦度。
现在 H a n d I n D e v i l \sf HandInDevil HandInDevil 想知道,如果祂失去了 t t t 个 [最高机密],祂最多能获得多少愉悦度?请对于所有 t ∈ [ n , ∑ k i ] t\in[n,\sum k_i] t∈[n,∑ki] 都求出答案。
数据范围与提示
因为
H
a
n
d
I
n
D
e
v
i
l
\sf HandInDevil
HandInDevil 很假,所以
n
⩽
1
0
5
n\leqslant 10^5
n⩽105 。因为祂很强,所以
0
⩽
a
i
,
j
⩽
1
0
9
0\leqslant a_{i,j}\leqslant 10^9
0⩽ai,j⩽109 。因为祂尤其短,所以
k
i
⩽
5
k_i\leqslant 5
ki⩽5 。“看开点,被击碎只会让数量变多!”
思路
反悔贪心
在 t t t 变大 1 1 1 时,只可能发生如下变化,进行一些 简单 的讨论即可。
- + 1 +1 +1
- + 2 − 1 +2-1 +2−1
- + 3 − 2 +3-2 +3−2
- + 3 − 1 − 1 +3-1-1 +3−1−1
- + 2 + 2 − 3 +2+2-3 +2+2−3
- + 4 − 3 +4-3 +4−3
- + 4 − 2 − 1 +4-2-1 +4−2−1
- + 4 − 1 − 1 − 1 +4-1-1-1 +4−1−1−1
这里有
8
8
8 种情况,每次要在
8
8
8 个堆内重新更新,复杂度是
O
(
K
log
K
)
\mathcal O(K\log K)
O(KlogK),其中
K
=
64
n
k
K=64nk
K=64nk 。而且写起来应该非常简单。
我们是冠军!
这个真的,太牛了。公屏们把兄弟打在 🐮🍺 上!
首先我们有一个 n a i v e \rm naive naive 的 O ( n 2 k 2 ) \mathcal O(n^2k^2) O(n2k2) 背包做法。即 f ( j ) f(j) f(j) 表示,付出 j j j 的代价,最多能得到多少愉悦值。
不难发现 f ( j ) f(j) f(j) 按照模 M = 12 M=12 M=12 的余数分类,每组都是凸的!即 f ( j ) − f ( j − M ) ⩾ f ( j + M ) − f ( j ) f(j)-f(j{\rm-}M)\geqslant f(j{\rm +}M)-f(j) f(j)−f(j−M)⩾f(j+M)−f(j) 总成立!
为什么呢?考虑 f ( j − M ) f(j{\rm-}M) f(j−M) 和 f ( j + M ) f(j{\rm+}M) f(j+M) 的方案之间的差异。第 i i i 句假话可能由 j 0 j_0 j0 的代价变为 j 2 j_2 j2 的代价,有一个 λ i = j 2 − j 0 \lambda_i=j_2-j_0 λi=j2−j0 的差。显然 ∑ λ i = 2 M \sum\lambda_i=2M ∑λi=2M 且 λ i ∈ [ − 4 , 4 ] \lambda_i\in[-4,4] λi∈[−4,4] 。
为了避免负数,我们先将其做一个处理。对于 λ i < 0 \lambda_i<0 λi<0,任取一个 λ x ⩾ ∣ λ i ∣ \lambda_{x}\geqslant|\lambda_i| λx⩾∣λi∣,将二者合并为 λ x + λ i \lambda_x+\lambda_i λx+λi 。如果不存在这样的 x x x,就找若干个 λ \lambda λ,使它们的和恰好大于 ∣ λ i ∣ |\lambda_i| ∣λi∣,合并为它们的和。显然是可以做到的——毕竟总和 = 2 M > 0 =2M>0 =2M>0 。并且合并得到的结果仍然在 [ 0 , 4 ] [0,4] [0,4] 范围内。
经过这样的处理,仍然满足 λ \lambda λ 是独立的。也就是说,可以独立地将若干个 λ \lambda λ 应用到 f ( j − M ) f(j{\rm-}M) f(j−M) 的方案上,使代价增加 ∑ i ∈ S λ i \sum_{i\in S}\lambda_i ∑i∈Sλi 。同时愉悦度的增加也是 ∑ i ∈ S ω i \sum_{i\in S}\omega_i ∑i∈Sωi 之类的。
打表暴搜 可以证明:对于任意整数集 S S S,如果 ∑ x ∈ S x = 2 M \sum_{x\in S}x=2M ∑x∈Sx=2M 且 ∀ x ∈ S , x ∈ [ 0 , 4 ] \forall x\in S,\;x\in[0,4] ∀x∈S,x∈[0,4],那么存在 T ⫅ S T\subseteqq S T⫅S 使得 ∑ x ∈ T x = M \sum_{x\in T}x=M ∑x∈Tx=M 。不要问为什么,反正是对的(暴搜代码在文末)。
也就是说,我们可以选出一个
λ
\lambda
λ 的子集
T
T
T,将其应用到
f
(
j
−
M
)
f(j{\rm-}M)
f(j−M) 的方案上,便可得到
f
(
j
)
f(j)
f(j) 。显然
T
T
T 的补集也可以做到这一点。只需要选择二者中,对愉悦度增加较大的一个,因为二者是独立地增加愉悦度。所以
f
(
j
)
⩾
f
(
j
−
M
)
+
f
(
j
+
M
)
2
f(j)\geqslant\frac{f(j{\rm-}M)+f(j{\rm+}M)}{2}
f(j)⩾2f(j−M)+f(j+M)
当然我们得到的只是一种
f
(
j
)
f(j)
f(j) 的方案,实际上的
f
(
j
)
f(j)
f(j) 可以更大,但是总是满足上面的不等式。而这个不等式就是凸性的证明。
证明了凸性,我们就会联想 闵科夫斯基和。两边都枚举模 M M M 的余数,然后做闵科夫斯基和。由于闵可夫斯基和是 O ( s i z e ) \mathcal O(size) O(size) 的,也就是子树大小,那肯定是要平均分配,也就是分治了。
时间复杂度 T ( N ) = 2 T ( N 2 ) + N k M ⋅ M 2 = N k M log N T(N)=2T({N\over 2})+\frac{Nk}{M}\cdot M^2=NkM\log N T(N)=2T(2N)+MNk⋅M2=NkMlogN 。
代码
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int_ x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
const int_ infty = (1ll<<60)-1;
const int MaxN = 100000;
const int MaxK = 5; // size of unit
const int MaxLen = MaxN*MaxK;
const int Step = 12;
void minkowski(int_ *a,int n,int_ *b,int m,int_ *c){
const int_ *end_a = a+n-1, *end_b = b+m-1;
*c = max(*c,(*a)+(*b)); // first one
for(++c; a!=end_a||b!=end_b; ++c)
if(b == end_b || (a != end_a && *(a+1)-*a > *(b+1)-*b))
++ a, *c = max(*c,(*a)+(*b));
else ++ b, *c = max(*c,(*a)+(*b));
}
int_ tmp[Step*2-1][MaxLen/Step+2];
int_ *dp[MaxN][Step]; int k[MaxN];
/// @brief solve [l,r), save into dp[l]
void solve(int l,int r){
if(r-l == 1) return ;
int m = (l+r)>>1;
solve(l,m), solve(m,r);
const int new_len = k[l]+k[m];
rep(i,0,(Step-1)<<1)
rep(j,0,new_len/Step)
tmp[i][j] = -infty;
rep(i,0,Step-1) rep(j,0,Step-1)
minkowski(dp[l][i],k[l]/Step+1,
dp[m][j],k[m]/Step+1,tmp[i+j]);
rep(i,Step,2*Step-2)
rep(j,0,new_len/Step-1)
tmp[i-Step][j+1] = max(
tmp[i-Step][j+1],tmp[i][j]);
int_ *_alloc = dp[l][0]; // re_alloc
rep(i,0,Step-1){
dp[l][i] = _alloc;
memcpy(dp[l][i],tmp[i],(new_len/Step+1)<<3);
_alloc += new_len/Step+1;
}
k[l] = new_len;
}
int_ memory_pool[MaxN*Step];
int main(){
int n = readint();
int_ *_alloc = memory_pool;
rep(i,0,n-1){
k[i] = readint()-1;
rep(j,0,k[i]){
dp[i][j] = _alloc ++;
*dp[i][j] = readint();
}
rep(j,k[i]+1,Step-1){
dp[i][j] = _alloc ++;
*dp[i][j] = -infty;
}
}
solve(0,n); // [0,n)
rep(i,0,k[0]){
writeint(dp[0][i%Step][i/Step]);
putchar(' ');
}
putchar('\n');
return 0;
}
后记
关于那个 s u m = 2 M sum=2M sum=2M 则存在子集 s u m = M sum=M sum=M 的东西,我挺好奇的,打了个表,也没看出任何规律。把暴搜验证的代码放在这里好了。
#include <cstdio>
#include <iostream>
#include <cstring>
#include <vector>
#include <algorithm>
using namespace std;
typedef long long int_;
# define rep(i,a,b) for(int i=(a); i<=(b); ++i)
# define drep(i,a,b) for(int i=(a); i>=(b); --i)
inline int readint(){
int a = 0; char c = getchar(), f = 1;
for(; c<'0'||c>'9'; c=getchar())
if(c == '-') f = -f;
for(; '0'<=c&&c<='9'; c=getchar())
a = (a<<3)+(a<<1)+(c^48);
return a*f;
}
inline void writeint(int_ x){
if(x < 0) putchar('-'), x = -x;
if(x > 9) writeint(x/10);
putchar((x-x/10*10)^48);
}
const int N = 3;
const int MaxAns = 1000;
int cnt[N+1], WANT;
bool dp[MaxAns+1];
bool check(){
memset(dp+1,0,WANT); dp[0] = true;
rep(i,1,N) drep(j,WANT,i)
for(int k=1; k<=cnt[i]&&!dp[j]; ++k)
dp[j] = dp[j-k*i];
return dp[WANT];
}
bool dfs(int t,int sum){
if(t == N+1)
return (sum != WANT*2) ? true : check();
for(cnt[t]=0; cnt[t]<=(WANT*2-sum)/t; ++cnt[t])
if(!dfs(t+1,sum+cnt[t]*t)) return false;
return true; // no error reported
}
int main(){
for(WANT=1; true; ++WANT)
if(dfs(1,0)) break;
printf("Step = %d\n",WANT);
return 0;
}