2024.6 -> 做题记录与方法总结

2024/6/15

P4363 [九省联考 2018] 一双木棋 chess

经典轮廓线dp
使用的关键在于发现状态数并不多,用 n n n 进制数来表现轮廓的状态

d p dp dp 的 转移 和 轮廓线 息息相关

img

如图,蓝色轮廓线状态只能转移到含一个紫色的状态
因为 $ 1 \leq n,m \leq 10$ 用 11 11 11 进制压缩状态就可以了

轮廓线状态压缩:

ll zip(int *now){
	ll res = 0;
	for(int i = n;i>=1;i--) res = res * 11 + now[i];
	return res; 
}

void unzip(ll S,int *now) {
	for(int i = 1;i<=n;i++) {
		now[i] = S % 11;
		S /= 11; 
	}
}	

AC-Code:

#include<bits/stdc++.h>
using namespace std;

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

typedef long long ll;

int n,m;
int a[11][11],b[11][11];

unordered_map<ll,ll> dp;

ll zip(int *now){
	ll res = 0;
	for(int i = n;i>=1;i--) res = res * 11 + now[i];
	return res; 
}

void unzip(ll S,int *now) {
	for(int i = 1;i<=n;i++) {
		now[i] = S % 11;
		S /= 11; 
	}
}	

const ll inf = 1e9;

ll dfs(ll S){
	if(dp.count(S)) return dp[S];
	int size = 0;
	int *now = new int[11];
	unzip(S,now);
	for(int i = 1;i<=n;i++) size += now[i];
	int re = (size & 1) ? inf:-inf;
	for(int i = 1;i<=n;i++) {
		if((i == 1 && now[i] < m) || (i != 1 && now[i] < now[i-1])) {
			now[i]++;
			if((size & 1)) re = min((ll)re,dfs(zip(now)) - b[i][now[i]]);
			else re = max((ll)re,dfs(zip(now)) + a[i][now[i]]);
			now[i]--;
		}
	}
	delete now;
	return dp[S] = re;
}

signed main() {
	n = rd(),m = rd();
	for(int i = 1;i<=n;i++) 
		for(int j = 1;j<=m;j++) 
			a[i][j] = rd();
	for(int i = 1;i<=n;i++) 
		for(int j = 1;j<=m;j++) 
			b[i][j] = rd();

	ll ed = 0;
	for(int i = 1;i<=n;i++) ed = ed * 11 + m;
	dp[ed] = 0;
	ll ans = dfs(0);
	wt(ans);
	return 0;
}

2024/6/16

codeforces Round 953 Div.2

A.Alice and Books

分成两堆,其中一堆编号最大一定是 n n n
只需要在 1 → ( n − 1 ) 1 \rightarrow (n - 1) 1(n1) 中找到最大值 a i a_i ai
答案为 a i + a n a_i + a_n ai+an

AC-Code:

#define useLL
#include<C:\Users\Ming\Desktop\workplace\head_file\all_function.h>
using namespace AllRangeApply_Define;
using namespace Atomic::fastSTD;

namespace my{

void solve() {
    int n = read();
    vector<int> a(n);
    for(int i = 0;i<n;i++) a[i] = read();

    int maxn = 0;
    for(int i = 0;i<n-1;i++) maxn = max(maxn,a[i]);
    write(a[n-1] + maxn);
    putchar('\n');
}

signed main(){
    int t = read();
    while(t--) solve();
    return 0;
}

}

signed main(){
    return my::main();
}
B.New Bakery

题面:


鲍勃决定开一家面包店。开业当天,他烤出了 n n n 个可以出售的包子。通常一个包子的价格是 a a a 个金币,但为了吸引顾客,鲍勃组织了以下促销活动:

  • 鲍勃选择某个整数 k k k ( 0 ≤ k ≤ min ⁡ ( n , b ) 0 \le k \le \min(n, b) 0kmin(n,b) )。
  • 鲍勃以修改后的价格出售第一批 k k k 个小面包。在这种情况下,售出的 i i i ( 1 ≤ i ≤ k 1 \le i \le k 1ik )个馒头的价格是 ( b − i + 1 ) (b - i + 1) (bi+1) 个硬币。
  • 剩下的 ( n − k ) (n - k) (nk) 个馒头以每个 a a a 个硬币的价格出售。

注意 k k k 可以等于 0 0 0 。在这种情况下,鲍勃将以每个 a a a 个硬币的价格出售所有馒头。

帮助鲍勃确定出售所有 n n n 个馒头所能获得的最大利润。


不难计算出利润表达式
w = k ( b + 1 ) − ∑ i = 1 k i + ( n − k ) a w = k(b + 1) - \sum_{i = 1}^k i + (n - k)a w=k(b+1)i=1ki+(nk)a

展开表达式
w = − 1 2 k 2 + ( b + 1 2 − a ) k + n a w = - \frac{1}{2}k^2 + (b + \frac{1}{2} - a)k + na w=21k2+(b+21a)k+na

开口向下, k = ( b + 1 2 − a ) k = (b + \frac{1}{2} - a) k=(b+21a) 时取最大值

计算出 k k k 后,因为 $0 \le k \le \min(n, b) 且 k \in N $,判断一下最大值在何处取即可

AC-Code:

#define useLL
#include<C:\Users\Ming\Desktop\workplace\head_file\all_function.h>
using namespace AllRangeApply_Define;
using namespace Atomic::fastSTD;

namespace my{

void solve() {
    int n = read(),a = read(),b = read();
    double k = b - a + 0.5;
    if(k < 0) k = 0;
    else if(k > min(n,b)) k = min(n,b);
    int ans = 0;
    double t = k;
    k = (int)k;
    ans = -0.5 * k * k + (b + 0.5 - a) * k + n * a;
    if((int)k < t && t < k + 1) {
        k = k + 1;
        ans = max(ans,(int)(-0.5 * k * k + (b + 0.5 - a) * k + n * a));
    }
    write(ans);
    putchar('\n');
}

signed main(){
    int t = read();
    while(t--) solve();
    return 0;
}

}

signed main(){
    return my::main();
}

2024/6/17

codeforces round 953 Div.2

C.Manhattan Permutations

注意到 k ∈ e v e n k \in even keven k ∈ o d d k \in odd kodd 的情况一定是 NO
然后将其反向排列,得到最大值
img

取得最大值

让我们来考虑一下,如果我们将同位排列中的 x x x 1 1 1 互换会发生什么。这种排列的曼哈顿值等于 ∣ x − 1 ∣ + ∣ x − 1 ∣ = 2 ⋅ ∣ x − 1 ∣ |x - 1| + |x - 1| = 2 \cdot |x - 1| x1∣+x1∣=2x1∣ 。那么从 0 0 0 2 ⋅ ( n − 1 ) 2 \cdot (n - 1) 2(n1) 的任何偶数 k k k 都可以得到。如果是 k > 2 ⋅ ( n − 1 ) k > 2 \cdot (n - 1) k>2(n1) ,那么让我们交换 1 1 1 n n n 元素。请注意,我们已经将问题缩小到了更小的 ( n − 2 ) (n - 2) (n2) ,只是现在的索引是从 2 2 2 开始,而不是 1 1 1 。现在我们可以像处理同位排列一样,将 2 2 2 与某个 x x x x > 1 x > 1 x>1 x < n x < n x<n )互换。排列的值会发生 ∣ x − 2 ∣ ⋅ 2 |x - 2| \cdot 2 x2∣2 的变化,因此可以得到从 0 0 0 2 ⋅ ( n − 1 ) + 2 ⋅ ( n − 3 ) 2 \cdot (n - 1) + 2 \cdot (n - 3) 2(n1)+2(n3) 的任意偶数 k k k 。请注意,如果我们继续这样做,最终将得到排列 [ n , n − 1 , . . . , 1 ] [n, n - 1, ..., 1] [n,n1,...,1] ,而我们已经证明了从 0 0 0 m a x k max_k maxk 的任何偶数 k k k 都是可以实现的。因此,如何在 O ( n ) O(n) O(n) 时间内重构排列本身是显而易见的。

然后构造

AC-Code:

#include <bits/stdc++.h>
using namespace std;

void solve() {
    int n;
    long long k;
    cin>>n>>k;
    long long sum = 0;
    for(int i = 0;i<n;i++) sum += abs(n - 1 - 2 * i);
    if(k & 1 || sum < k) {
        puts("NO");
        return;
    }
    puts("Yes");
    vector<int> p(n);
    k /= 2;
    iota(p.begin(),p.end(),0);
    for(int i = 0;k > 0;i++) {
        if(k >= n - 1 - 2 * i) {
            swap(p[i],p[n - 1 - i]);
            k -= n - 1 - 2 * i;
        }else {
            swap(p[i],p[i + k]);
            k = 0;
        }
    }
    for(int i = 0;i<n;i++) cout<<p[i] + 1<<' ';
    cout<<'\n';
}

signed main() {
    int t = 1;
    cin>>t;
    while(t--) solve();
    return 0;
}

2024/06/23

codeforces round 953 Div.2

D.Elections

难绷,赛时想的差不多了,然后偏到数据结构

如果候选人 i i i 最初没有获胜(当没有人被剔除时),那么为了使他们获胜,我们必须剔除所有 i d id id 小于 i i i 的候选人。因为如果仍然有人的 i d id id 小于 i i i ,那么候选人 i i i 的票数就不会增加,而其他人的最大票数也不会减少。

让我们先找出选举的获胜者,对于他们来说,答案是 0 0 0 ,对于所有其他候选人,我们需要删除所有 i d id id 小于 i i i 的人。

但有时仅仅删除这些人可能还不够,因此我们需要额外删除几名候选人,这样他们的粉丝就会把票投给我们。

请注意,此时只需删除一个最大值为 a i a_i ai 的候选人,那么我们就一定会赢,因此候选人 i i i 的答案要么是 0 0 0 ,要么是 i i i ,要么是 i + 1 i + 1 i+1

因此,我们最终得到了一个在 O ( n ) O(n) O(n) 中有效的解。

AC-code:

#include<bits/stdc++.h>
using namespace std;

void solve() {
    int n,c;
    cin>>n>>c;

    vector<int> a(n);
    for(int i = 0;i<n;i++) cin>>a[i];
    if(n == 1) {
        cout<<0<<'\n';
        return;
    }
    int mx = *max_element(a.begin() + 1,a.end());
    int mxc = max(a[0] + c,mx);
    int winer = mxc == a[0] + c ? 0:find(a.begin() + 1,a.end(),mx) - a.begin();
    long long sum = c;
    for(int i = 0;i<n;sum += a[i],i++) {
        int ans = 0;
        if(i == winer) ans = 0;
        else if(sum + a[i] >= mx) ans = i;
        else ans = i + 1;
        cout<<ans<<" \n"[i == n-1];
    }
}

signed main() {
    int t;
    cin>>t;
    while(t--) solve();
    return 0;
}

2024/06/28

北京时间:0:46

不得不感慨codeforce的阴间比赛时间
还有 jiangly 太强了

Educational Codeforces Round 167 (Rated for Div. 2)

A. Catch the Coin

赛时稀里糊涂就过了

注意到我们是先手

所以, y y y 掉到 − 2 -2 2 就无药可救了

否则,一定可以赶得上

AC-code:


#include<bits/stdc++.h>
using namespace std;

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

signed main() {

	int n = rd();
	while(n--) {
		int x = rd(),y = rd();

		if(y < -1) {
			puts("NO");
			continue;
		}
		puts("YES");

	}

	return 0;
}
B - Substring and Subsequence

赛时没过,想复杂了
其实暴力就可以了

AC-code:

#include<bits/stdc++.h>
using namespace std;

void solve() {
	string a,b;
	cin>>a>>b;

	int ans = b.size();
	for(int i = 0;i<b.size();i++) {
		int k = i;
		for(int j = 0;j<a.size();j++) {
			if(k < b.size() && a[j] == b[k]) k++;
		}
		ans = min(ans,i + (int)b.size() - k);
	}
	cout<<ans + a.size()<<'\n';
}

signed main() {
	int T;
	cin>>T;
	while(T--) solve();
	return 0;
}
C - Two Movies

这个赛时想的很快

求一种选择,使得最小值最大

因为当 一个人对两个评价为 一个不坏和一个坏,我们必然贪心的选择不坏的那个

没有人会给自己找别扭,不是吗

所以对于 { 1 , − 1 } , { 0 , − 1 } , { 0 , 0 } , { 1 , 0 } \{1,-1\},\{0,-1\},\{0,0\},\{1,0\} {1,1},{0,1},{0,0},{1,0} 评价都可以无脑判断

那么 对于 { 1 , 1 } \{1,1\} {1,1} { − 1 , − 1 } \{-1,-1\} {1,1} 呢?

维护最小值最大,肯定是贪心让大的多付出

哪个是大的呢?

因为

所以对于 { 1 , − 1 } , { 0 , − 1 } , { 0 , 0 } , { 1 , 0 } \{1,-1\},\{0,-1\},\{0,0\},\{1,0\} {1,1},{0,1},{0,0},{1,0} 评价都可以无脑判断

我们可以先将所以的正面评价都累加起来,后面再考虑有舍有得

回到 对于 { 1 , 1 } \{1,1\} {1,1} { − 1 , − 1 } \{-1,-1\} {1,1}

∑ a \sum a a ∑ b \sum b b 中较大的一个里面去扣,就结束了

AC-code:

#include<bits/stdc++.h>
using namespace std;

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

void solve() {
	int n = rd();
	vector<int> a(n);
	for(int i = 0;i<n;i++) a[i] = rd();
	vector<int> b(n);
	for(int i = 0;i<n;i++) b[i] = rd();
	int maxn = 0,sa = 0,sb = 0;
	for(int i = 0;i<n;i++) if(a[i] == 1) sa++;
	for(int i = 0;i<n;i++) if(b[i] == 1) sb++;

	for(int i = 0;i<n;i++) {
		if(a[i] == -1 && b[i] == -1) {
			if(sa >= sb) sa--;
			else sb--;
		}
		if(a[i] == 1 && b[i] == 1) {
			if(sa >= sb) sa--;
			else sb--;
		}
	}
	wt(min(sa,sb));
	putchar('\n');
}

signed main() {

	int t = rd();
	while(t--) solve();

	return 0;
}

CF708E Student’s Camp

显然,我们不难找到

f i , l , r = g l − 1 g m − r ∑ max ⁡ ( l , l ′ ) ≤ min ⁡ ( r , r ′ ) f i − 1 , l ′ , r ′ \large f_{i,l,r} = g_{l-1} g_{m - r} \sum_{\max(l,l^{\prime}) \leq \min(r,r^{\prime})} f_{i-1,l^{\prime},r^{\prime}} fi,l,r=gl1gmrmax(l,l)min(r,r)fi1,l,r

其中 g i g_i gi 代表着 k k k 天正好消失 i i i 个格子,

g i = ( k i ) p i ( 1 − p ) k − i g_i = \dbinom{k}{i}p^i(1-p)^{k-i} gi=(ik)pi(1p)ki

f i , l , r f_{i,l,r} fi,l,r 是第 i i i 行恰保留了 l , r l,r l,r 0 ∼ i 0\sim i 0i 行还连通的概率

但是, f f f 数组达到了 O ( n m 2 ) O(nm^2) O(nm2) 的规模

显然不可接受

这个时候就需要 正难则反

我们考虑扣掉不连通的情况

f i , r f_{i,r} fi,r 为 第 i i i 行右端点为 r r r,第 0 0 0 行到 第 i i i 行都连通的概率

在转移的时候,枚举左端点 l l l,计算与之相交的区间和

因为网格是对称的,那么 f i , r f_{i,r} fi,r 也可以指 第 i i i 行的左端点是 m − r + 1 m - r + 1 mr+1,右端点为 m m m,连通的概率

得到转移:
f i , r = ( ∑ l = 1 r p l − 1 ) ⋅ p m − r ⋅ ( ∑ j = 1 m f i − 1 , j − ∑ j = 1 l − 1 f i − 1 , j − ∑ j = 1 m − r f i − 1 , j ) f_{i,r} = (\sum_{l = 1}^rp_{l-1}) \cdot p_{m-r} \cdot (\sum_{j = 1}^m f_{i-1,j} - \sum_{j = 1}^{l-1} f_{i-1,j} - \sum_{j = 1}^{m - r} f_{i-1,j}) fi,r=(l=1rpl1)pmr(j=1mfi1,jj=1l1fi1,jj=1mrfi1,j)

当然,这一堆的求和符号暗示我们,用 前缀和 的时候,到了

p i p_{i} pi 前缀和为 a i a_i ai p i h i p_ih_i pihi 的前缀和为 b i b_i bi

那么有:

f i , r = ∑ l = 1 r p l − 1 p m − r ( h m − h l − 1 − h m − r ) f_{i,r} = \sum_{l = 1}^rp_{l-1}p_{m-r}(h_m - h_{l-1} - h_{m-r}) fi,r=l=1rpl1pmr(hmhl1hmr)

f i , r = p m − r [ ( h m − h m − r ) ∑ l = 1 r p l − 1 − ∑ l = 1 r p l − 1 h l − 1 ] f_{i,r} = p_{m-r}[(h_m - h_{m-r})\sum_{l=1}^rp_{l-1} - \sum_{l=1}^rp_{l-1}h_{l-1}] fi,r=pmr[(hmhmr)l=1rpl1l=1rpl1hl1]

f i , r = p m − r [ ( h m − h m − r ) a r − 1 − b r − 1 ] f_{i,r} = p_{m-r}[(h_m - h_{m-r})a_{r-1} - b_{r-1}] fi,r=pmr[(hmhmr)ar1br1]

这样,转移式简单多了,时间复杂度优化到了 O ( n m ) O(nm) O(nm)

AC-code:

#include<bits/stdc++.h>
using namespace std;

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int mod = 1e9+7;
const int N = 1.5e3+10;
const int S = 1e5+10;

#define int long long

void reduce(int &x){x += x >> 31 & mod;}

int mul(int a,int b){
	return a * b % mod;
}

int pow(int x,int k,int res = 1){
	while(k){
		if(k & 1) res = (res * x) % mod;
		x = (x * x) % mod;
		k >>= 1;
	} 
	return res;
}

void fma(int &x,int y,int z) {
	x = (y * z + x) % mod;
}

int n,m,k;
int fac[S],inv[S];
int ps[N],pre[N];
int f[N][N];

int C(int a,int b){
	return fac[a] * inv[b] % mod * inv[a - b] % mod;
}

signed main() {
	fac[0] = fac[1] = inv[0] = inv[1] = 1;
	for(int i = 2;i<S;i++) {
		fac[i] = mul(fac[i-1],i);
		inv[i] = mul(inv[mod % i],mod - mod/i);
	}
	for(int i = 2;i<S;i++) inv[i] = mul(inv[i-1],inv[i]);
	int a,b;
	n = rd(),m = rd(),a = rd(),b = rd(),k = rd();
	int P = pow(b,mod - 2,a);

	for(int i = 0;i <= m && i <= k;i++) ps[i] = pow(P,i,pow(1 + mod - P,k - i,C(k,i)));
	pre[0] = ps[0];
	for(int i = 1;i<=m;i++) reduce(pre[i] = ps[i] + pre[i-1] - mod);

	f[0][m] = 1;
	for(int i = 1;i<=n;i++) {
		static int tp[N],ts[N];
		for(int j = 1;j<=m;j++) {
			reduce(tp[j] = tp[j-1] + f[i-1][j] - mod);
			fma(ts[j] = ts[j-1],ps[j],tp[j]);
		}
		for(int r = 1;r<=m;r++) {
			int sm = mul(pre[r-1],tp[m] - tp[m-r] + mod);
			reduce(sm -= ts[r-1]);
			f[i][r] = mul(sm,ps[m-r]);
		}
	}
	int ans = 0;
	for(int i = 1;i<=m;i++) reduce(ans += f[n][i] - mod);
	wt(ans);


	return 0;
}

这个题还有一点容斥的思想

当我们发现三维存不下的时候,就需要想一想用容斥能不能省掉一维的空间

这个题就是这样,我们会惊讶的发现 l , r l,r l,r 太大,所以 l , r l,r l,r 是我们重点关照的对象

那么,我们能不能想出砍掉一个端点,以至于去使用 正难则反 的思想呢?

就算我们推出了式子,我们能否有清晰的逻辑去实现前缀和呢?

P3648 [APIO2014] 序列分割

题面:

题目描述

你正在玩一个关于长度为 n n n 的非负整数序列的游戏。这个游戏中你需要把序列分成 k + 1 k + 1 k+1 个非空的>块。为了得到 k + 1 k + 1 k+1 块,你需要重复下面的操作 k k k 次:

选择一个有超过一个元素的块(初始时你只有一块,即整个序列)

选择两个相邻元素把这个块从中间分开,得到两个非空的块。

每次操作后你将获得那两个新产生的块的元素和的乘积的分数。你想要最大化最后的总得分。

输入格式

第一行包含两个整数 n n n k k k。保证 k + 1 ≤ n k + 1 \leq n k+1n

第二行包含 n n n 个非负整数 a 1 , a 2 , ⋯   , a n a_1, a_2, \cdots, a_n a1,a2,,an ( 0 ≤ a i ≤ 1 0 4 ) (0 \leq a_i \leq 10^4) (0ai104),表示前文所述的序列。

输出格式

第一行输出你能获得的最大总得分。

第二行输出 k k k 个介于 1 1 1 n − 1 n - 1 n1 之间的整数,表示为了使得总得分最大,你每次操作中分开两个块的位置。第 i i i 个整数 s i s_i si 表示第 i i i 次操作将在 s i s_i si s i + 1 s_{i + 1} si+1 之间把块分开。

如果有多种方案使得总得分最大,输出任意一种方案即可。

样例 #1
样例输入 #1
7 3
4 1 3 4 0 2 3
样例输出 #1
108
1 3 5
提示

你可以通过下面这些操作获得 108 108 108 分:

初始时你有一块 ( 4 , 1 , 3 , 4 , 0 , 2 , 3 ) (4, 1, 3, 4, 0, 2, 3) (4,1,3,4,0,2,3)。在第 1 1 1 个元素后面分开,获得 4 × ( 1 + 3 + 4 + 0 + 2 + 3 ) = 52 4 \times (1 + 3 + 4 + 0 + 2 + 3) = 52 4×(1+3+4+0+2+3)=52 分。

你现在有两块 ( 4 ) , ( 1 , 3 , 4 , 0 , 2 , 3 ) (4), (1, 3, 4, 0, 2, 3) (4),(1,3,4,0,2,3)。在第 3 3 3 个元素后面分开,获得 ( 1 + 3 ) × ( 4 + 0 + 2 + 3 ) = 36 (1 + 3) \times (4 + 0 + 2 + 3) = 36 (1+3)×(4+0+2+3)=36 分。

你现在有三块 ( 4 ) , ( 1 , 3 ) , ( 4 , 0 , 2 , 3 ) (4), (1, 3), (4, 0, 2, 3) (4),(1,3),(4,0,2,3)。在第 5 5 5 个元素后面分开,获得 ( 4 + 0 ) × ( 2 + 3 ) = 20 (4 + 0) \times (2 + 3) = 20 (4+0)×(2+3)=20 分。

所以,经过这些操作后你可以获得四块 ( 4 ) , ( 1 , 3 ) , ( 4 , 0 ) , ( 2 , 3 ) (4), (1, 3), (4, 0), (2, 3) (4),(1,3),(4,0),(2,3) 并获得 52 + 36 + 20 = 108 52 + 36 + 20 = 108 52+36+20=108 分。

不难看出,明显的斜率优化典题

首先,通过乘法结合律,分块的顺序不会改变分值

因为,对于 块 a b c abc abc a ( b + c ) + b c = a b + ( a + b ) c = a b + a c + b c a(b + c) + bc = ab + (a + b)c = ab + ac + bc a(b+c)+bc=ab+(a+b)c=ab+ac+bc

那么,通过一个 d p dp dp,做一个内嵌套 (每次分的位置都为最优策略中第一次分开的位置)

得到转移式:
f i , k + 1 = max ⁡ j < i { f j , k + ( s i − s j ) s j } f_{i,k+1} = \max_{j < i}\{f_{j,k} + (s_i - s_j)s_j \} fi,k+1=j<imax{fj,k+(sisj)sj}

其中 s i s_i si 为 序列 0 ∼ i 0 \sim i 0i 的前缀和

一看 k → k + 1 k \rightarrow k + 1 kk+1,滚动数组少不了

这样,式子变成了

f i = max ⁡ j < i { f j + ( s i − s j ) s j } f_i = \max_{j<i}\{f_j + (s_i - s_j)s_j \} fi=j<imax{fj+(sisj)sj}

那么对于 i i i 的最优决策点 j j j 和不优决策点 k k k(默认 j < k j < k j<k

f j + ( s i − s j ) s j > f k + ( s i − s k ) s k f_j + (s_i - s_j)s_j > f_k + (s_i - s_k)s_k fj+(sisj)sj>fk+(sisk)sk
( f j − s j 2 ) − ( f k − s k 2 ) > ( s k − s j ) s i (f_j - s_j^2) - (f_k - s_k^2) > (s_k - s_j)s_i (fjsj2)(fksk2)>(sksj)si
( f j − s j 2 ) − ( f k − s k 2 ) − s j − ( − s k ) > s i \frac{(f_j - s_j^2) - (f_k - s_k^2)}{-s_j - (-s_k)} > s_i sj(sk)(fjsj2)(fksk2)>si

即:
f j − s j 2 s i − f k − s k 2 s i − s j − ( − s k ) > 1 \frac{\frac{f_j - s_j^2}{s_i} - \frac{f_k - s_k^2}{s_i} }{-s_j - (-s_k)} > 1 sj(sk)sifjsj2sifksk2>1

是一个 y = f i − s i 2 s i   &   x = − s i y = \frac{f_i - s_i ^ 2}{s_i}\ \& \ x = -s_i y=sifisi2 & x=si
我们看到决定优决策点的斜率是大于 1 1 1

我们可将每个点看做 P ( − s i , f i − s k 2 ) P(-s_i,f_i - s_k ^ 2) P(si,fisk2),维护一个横坐标单增的下凸包 ()

此题就解决了!

AC-code:

#include<bits/stdc++.h>
using namespace std;

#define int long long

int rd() {
	int x = 0, w = 1;
	char ch = 0;
	while (ch < '0' || ch > '9') {
		if (ch == '-') w = -1;
		ch = getchar();
	}
	while (ch >= '0' && ch <= '9') {
		x = x * 10 + (ch - '0');
		ch = getchar();
	}
	return x * w;
}

void wt(int x) {
	static int sta[35];
	int f = 1;
	if(x < 0) f = -1,x *= f;
	int top = 0;
	do {
		sta[top++] = x % 10, x /= 10;
	} while (x);
	if(f == -1) putchar('-');
	while (top) putchar(sta[--top] + 48);
}

const int N = 100005;

int f[N],g[N],n,k,s[N],pre[205][N];

double slope(int a,int b){
	int x1 = -s[a],x2 = -s[b];
	int y1 = g[a] - s[a] * s[a],y2 = g[b] - s[b] * s[b];
	if(x1 == x2) return -1e18; 
	return (double)(y1 - y2) / (double)(x1 - x2);
}

signed main() {

	n = rd(),k = rd();
	for(int i = 1;i<=n;i++) {
		s[i] = rd();
		s[i] += s[i - 1];
	}

	static int q[N],head = 0,tail = 0; //[head,tail)

	for(int d = 1;d<=k;d++){
		for(int i = 1;i<=n;i++) g[i] = f[i];

		head = 0,tail = 0;
		for(int i = 1;i<=n;i++) {
			while(tail - head >= 2 && slope(q[head],q[head + 1]) <= s[i]) head++;
			if(tail > head) {
				int &j = q[head];
				pre[d][i] = j;
				f[i] = g[j] + (s[i] - s[j]) * s[j];
			}
			while(tail - head >= 2 && slope(q[tail - 1],q[tail - 2]) >= slope(q[tail - 1],i)) tail--;
			q[tail++] = i;
		}
	}

	wt(f[n]);putchar('\n');

	for(int x = pre[k][n];k;x = pre[--k][x]) wt(x),putchar(' ');

	return 0;
}

这里也附上 《深入浅出提高版》的向量维护凸包
code:

#define useLL
#include <C:\Users\Administrator\Desktop\workspace\head_file\all_function.h>
using namespace Atomic::fastSTD;
using namespace AllRangeApply_Define;

namespace my{

const int N = 1e5+5;
typedef long long ll;

ll f[2][N],pre[210][N];
int a[N],n,k,head,tail;

struct vec
{
	int id,x;
	ll y;
	ll operator ()(int v){
		return (ll)x * v + y;
	}
}q[N];

ll cross(vec a,vec b,vec c) {
	a.x -= b.x;
	a.y -= b.y;
	b.x -= c.x;
	b.y -= c.y;
	return b.x * a.y - a.x * b.y;
}


signed main() {
	n = rd(),k = rd();
	for(int i= 1;i<=n;i++) a[i] = rd();
	for(int i= 1;i<=n;i++) a[i] += a[i-1];

	for(int T = 1;T<=k;T++) {
		head = tail = 0;
		q[0] = (vec) {0,0,0};
		for(int i = 1;i<=n;i++) {
			while(head < tail && q[head](a[i]) <= q[head + 1](a[i])) ++head;
			f[T & 1][i] = q[head](a[i]);
			pre[T][i] = q[head].id;
			vec x = (vec) {i,a[i],f[T&1^1][i] - (ll)a[i] * a[i]};
			while(head < tail && cross(q[tail-1],q[tail],x) <= 0) --tail;
			q[++tail] = x;
		}
	}

	wt(f[k & 1][n]);
	putchar('\n');
	int now = n;
	for(int i = k;i>=1;i--) {
		wt(pre[i][now]);
		putchar(' ');
		now = pre[i][now];
	}
	return 0;
}

}

signed main() {
	return my::main();
}
  • 13
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值