考魔怔了
题发下来一看,直接傻掉,感觉都不好写
不过确实带给我很多经验
具体来说
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 的最小生成树即可! (太妙了啊)
总体来说,两个启示 :
- 建图的功能很强大,不仅能跑 i -> j 最小变换代价,还可以跑出所有元素都碰一遍的最小权值和
- 遇到一些 比较特殊的边 时 ,完全可以考虑用虚拟节点来转化
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] n∗m−∑b[i]
显然,部分 m 可以省掉 :
我要把 n n n 个物品,分成 n 2 \frac n2 2n 组,每组的价格之和 v v v 必定满足 0 ≤ v < 2 ∗ m 0 \leq v < 2*m 0≤v<2∗m
- 对于 v = 0 v = 0 v=0 ,那么这一组可以省掉 2 ∗ m 2*m 2∗m
- 对于 0 < v ≤ m 0<v\leq m 0<v≤m ,这一组一张代金卷即可,省掉 m m m
- 对于 m < v < 2 ∗ m m<v<2*m m<v<2∗m ,这一组不可省
总钱数、组数一定,因此我的策略一定是尽可能多的配出 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 ;
}
处理好细节,实际上就是很常规的贪心,排完序 最大 与 最小匹配 为什么我不会
总结:
第一点,心态问题,看题看全面,切忌死磕一道题!
第二点,要探求本质,多打表(这道题很关键的一点就是最优方案只与分出来的组数有关)
第三点,贪心的套路要熟悉,额,刷题!