[隐式建图 \ 倍增 \ 贪心 ] 2022.10.23 模拟赛总结

考魔怔了

题发下来一看,直接傻掉,感觉都不好写

不过确实带给我很多经验

具体来说

T1

额,跳过了,申清题意会发现质因子分解一下就行

T2

一道具有启发性意义的题目
在这里插入图片描述
首先很容易想到:

对于任意两个串 s1 , s2 如何快速求出变换的代价?

显然正是 lcs (最长公共子序列) : s1 -> lcs -> s2

那么代价就是 s1.len + s2.len - 2*lcs + K

到这里,其实已经有图论特征 : 存在两个点之间的变换

而问题是全部串打印出来的总代价 ?

其实正是每个点连通,最小的边权和

最小生成树 !

可是,与常规的还有点小区别 :

可以选择变换 , 同时也可选择直接以 l[i] 的代价直接抵达

由 x 到 y 和 由 y 到 x 连通费用不一定相等了 ( l[x] != l[y] ) 有向图最小生成树?

一定记住:当某些点可以一定代价直接抵达时,完全可以创造出一个虚拟节点 n + 1 ,从 n+1 抵达这些节点

那么,add ( n+1 , i , l[i] ) ,跑 1~n+1 的最小生成树即可! (太妙了啊)

总体来说,两个启示 :

  1. 建图的功能很强大,不仅能跑 i -> j 最小变换代价,还可以跑出所有元素都碰一遍的最小权值和
  2. 遇到一些 比较特殊的边 时 ,完全可以考虑用虚拟节点来转化
T3

在这里插入图片描述

简单分析一下: 若起点确定了,则整个积分序列就已确定

于是乎,简单的枚举,正是第一档分数 30pts

分析算法中效率低的地方 : 显然很多序列重复枚举了 !

假设从 i 点开始往后跳,跳到 k 了,那么以 k 为起点的序列会被枚举一遍

那么,任意能够跳到 k 的点 (包括 k 本身) ,都会使得 k 序列被重新全部枚举一遍 !

低效,太低效了!!!

很久以前,蒟蒻的我只会用 f[k] 来表示以 k 为节点 , 整个序列的和

这道题显然完全不够用 , 信息量太少了!

我需要知道整个 k 序列的值,但同时我完全可能用到 k 序列任意一个前缀序列的值

f[k][j] 二维?显然必须

若单纯以此表示 [k,j] 整个序列的话 ,不过是在避开时间而用空间苟活罢了

有没有一种算法,保证充足信息量的同时,空间上的效率也较优?

那么,就需要拿出我尘封多年的算法 ------ 倍增

为什么可以倍增?

任意一个 前缀序列 都可以被划分为 若干个长度为 2 的整次幂的序列

一遍预处理后,保证计算过的不被重复计算

Code

#include<bits/stdc++.h>
using namespace std ;
int n , m , K ;
int a[200100] , d[200100] , ans ;
int nxt[200100][25] ;// 以 i 为起点,往后跳 2^j 步的后继 
int sum[200100][25] ;// 以 i 为起点,序列长度为 2^j , 总和
int f[200100][25] ; // 以 i 为起点 ,序列长度为 2^j , 最大前缀和
int main () 
{
	scanf("%d%d%d" , &n , &m , &K ) ;
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &a[i] ) ;
	}
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d" , &d[i] ) ;
		sum[i][0] = d[i] ;
		f[i][0] = max( 0 , d[i] ) ;
	}
	int nx = 1 ;
	for(int i = 1 ; i <= n ; i ++ ) { // 额,这是正解的做法,像我这种蒟蒻只会直接 lower_bound
		while( nx <= n && a[nx] - a[i] <= K ) nx ++ ; 
		if( a[nx] - a[i] > K ) nxt[i][0] = nx ;
		else nxt[i][0] = n + 1 ; // 保证后面的递推不出现错误,sum[n+1] 与 f[n+1]均为 0
	}
	for(int T = 1 ; T <= 20 ; T ++ ) { //倍增的递推中,^项要为最外层 (阶段)
		for(int i = 1 ; i <= n ; i ++ ) {
			if( nxt[i][T-1] != n + 1 ) {
				nxt[i][T] = nxt[nxt[i][T-1]][T-1] ; // 跳 k 步 === 先跳 k/2 步,再跳 k/2 步
			}
			else {
				nxt[i][T] = n + 1 ; 
			}
		}
	}
	for(int T = 1 ; T <= 20 ; T ++ ) { 
		for(int i = 1 ; i <= n ; i ++ ) {
			sum[i][T] = sum[i][T-1] + sum[nxt[i][T-1]][T-1] ;
			f[i][T] = max ( f[i][T-1] , sum[i][T-1] + f[nxt[i][T-1]][T-1] ) ;// 要么取一半以内,要么以外 ,类似于 DP 的思路,整个代码核心
		}
	}
	for(int i = 1 ; i <= n ; i ++ ) { // 从 i 往后跳 m-1 
		int x = m , j = i , now = 0 , p = 0 ; // 把 m 划分成若干长度为 2 的整次幂的区间 ,每个区间上更新答案 
		while( x ) {
			if( x&1 ) {
				ans = max ( ans , now + f[j][p] ) ;
				now += sum[j][p] ; // 很精彩的一步
				j = nxt[j][p] ;
			}
			x >>= 1 ;
			p ++ ;
		} 
	}
	cout << ans ;
	return 0 ;
}

(还真就是带着倍增打模拟)

总结一下:

倍增的核心就是 预处理小区间 + 拼凑大区间

预处理:递推,一定以 ^ 作为最外层循环 (阶段) ,考虑 小一号区间 和 大区间 的转移式

拼凑:对大区间长度作二进制划分 (对于这道题,显然从大到小和从小到大枚举都行) ,考虑划分出的每个序列对答案的影响

T4

在这里插入图片描述

额,考试时是能发现贪心特征的,但心态已经被T3搞崩了捏

其实题解写的有点复杂,换种思路来

首先,我们明确一点,每种奶茶价格肯定是要先 %m 的 (代金卷无限使用)

那么不妨假设一种极端情况: 即每一杯奶茶( %m 后 )都需要一张代金卷,浪费的总钱数为 n ∗ m − ∑ b [ i ] n*m - \sum_{}b[i] nmb[i]

显然,部分 m 可以省掉 :

我要把 n n n 个物品,分成 n 2 \frac n2 2n 组,每组的价格之和 v v v 必定满足 0 ≤ v < 2 ∗ m 0 \leq v < 2*m 0v<2m

  1. 对于 v = 0 v = 0 v=0 ,那么这一组可以省掉 2 ∗ m 2*m 2m
  2. 对于 0 < v ≤ m 0<v\leq m 0<vm ,这一组一张代金卷即可,省掉 m m m
  3. 对于 m < v < 2 ∗ m m<v<2*m m<v<2m ,这一组不可省

总钱数、组数一定,因此我的策略一定是尽可能多的配出 1. 2. 避免 3.

剩下的钱 只与每组能不能小于等于 m 有关,与每一组比 m 小多少无关 (不管小多少都是省 m )

这就是核心思路,具体实现:

首先我要尽可能将花费为 0 的两两配对 ;

然后,比较小的数肯定很容易省 m ,因此考虑如何尽可能多的使大数省掉 m (先排好序)

      当然是给最大数配上当前最小的数 !

      如果配完之后成功了,省掉 m 了,就这么配

      如果仍然大于 m :当前最大数即使配上最小的数都不能满足要求,含当前最大数的那一组必定会用掉 2*m

      此时就应该给最大数配一个次大的(浪费掉不够优的,要把有希望配出来的数留到最后

最后额外判断一下边界即可

Code

#include<bits/stdc++.h>
using namespace std ;
int n ;
struct nn 
{
	int a , b ;
}p[100010] , t[100010] ;
int l ;
bool lal ( nn x , nn y ) 
{
	return x.b < y.b ;
}
long long ans , m ; 
long long res ;
void qc() // 去重
{
	sort( t+1 , t+n+1 , lal ) ;
	p[++l] = t[1] ;
	for(int i = 2 ; i <= n ; i ++ ) {
		if( t[i].b == t[i-1].b ) {
			p[l].a += t[i].a ;
		}
		else p[++l] = t[i] ;
	}
}
int main () 
{
	scanf("%d%lld" , &n , &m ) ; 
	for(int i = 1 ; i <= n ; i ++ ) {
		scanf("%d%d" , &t[i].a , &t[i].b ) ;
		t[i].b %= m ;
		res = res + ( m - t[i].b )* t[i].a ; 
	}
	qc() ;
	int i = 1 , j = l ;
	if( p[i].b == 0 ) { // 1.将 0 匹配
		ans += p[i].a / 2 * 2 ; // ans 记录当前省下多少个 m 
		p[i].a %= 2 ;
		if( !p[i].a ) i ++ ; 
	}

	while( i < j ) { // 2.大数与小数匹配
		if( p[j].b + p[i].b <= m ) { // 能省 
			if( p[j].a <= p[i].a ) {
				ans += p[j].a ;
				p[i].a -= p[j].a ;
				p[j].a = 0 ;
				if( p[i].a == 0 ) i ++ ; 
				j -- ; 
			}
			else { 
				ans += p[i].a ; 
				p[j].a -= p[i].a ; 
				p[i].a = 0 ; 
				i ++ ; 
			} 
		}
		else { // j 作为最大数,一定不能被省 , 跟次大数消 
			p[j].a %= 2 ;
			if( p[j].a ) p[--j].a -- ;
			if( !p[j].a ) j -- ;
		}
	}
	
	if( i == j ) { // 3.处理边界,位于中间的自己和自己配
		if( p[i].b * 2 <= m ) {
			ans += p[i].a / 2 ;
		}
	}
	cout << res - ans*m ;
	return 0 ;
}

处理好细节,实际上就是很常规的贪心,排完序 最大 与 最小匹配 为什么我不会

总结:

第一点,心态问题,看题看全面,切忌死磕一道题!

第二点,要探求本质,多打表(这道题很关键的一点就是最优方案只与分出来的组数有关)

第三点,贪心的套路要熟悉,额,刷题!

  • 8
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值