[星标][技巧][性质] 2023.5.11 模拟赛总结


第一次ACM赛制,减去吃饭时间大概 4h 30min , 8道题


T1

在这里插入图片描述
A , B > = 1 A,B >= 1 A,B>=1

n < = 1 0 5 n <= 10^5 n<=105

性质题:

将跳到的位置的序列抽象出来,即 A   ,   A − B   , A − B + A   ,   2 ∗ ( A − B ) ,   2 ∗ ( A − B ) + A   . . . . . A \ , \ A-B\ , A-B+A\ , \ 2*(A-B) , \ 2*(A-B)+A \ ..... A , AB ,AB+A , 2(AB), 2(AB)+A .....分奇偶来看:

A − B = x A-B = x AB=x

偶数步显然为: [   x   ,   2 x   , 3 x   . . . . . . ] [\ x\ , \ 2x\ , 3x\ ......] [ x , 2x ,3x ......]

奇数步看起来很难讨论 A A A 的取值,那么不妨反过来看

只有奇数步才能跳到终点 n − 1 n-1 n1 ( 往前跳时才能跳到 n − 1 n-1 n1 )

那么奇数步: [   . . . . . .    n − 1 − 2 x   ,   n − 1 − x   ,   n − 1   ] [\ ...... \ \ n-1-2x \ , \ n-1-x \ ,\ n-1 \ ] [ ......  n12x , n1x , n1 ]

所以我们只需要枚举 x x x偶数步结束的位置,即可复原出每一种跳跃方式

而对于一个确定的 x x x 值,在 枚举偶数步结束的位置 的过程中实际上对答案的贡献 只增不减

假设当前跳跃方式为:

[ x   ,   2 x   . . .   p x   ] [x\ ,\ 2x\ ...\ px\ ] [x , 2x ... px ]

[ n − 1   ,   n − 1 − x   . . .   n − 1 − p x   ] [n-1\ ,\ n-1-x \ ... \ n-1-px\ ] [n1 , n1x ... n1px ]

那么下一步

[ x   ,   2 x   . . .   p x   , ( p + 1 ) x   ] [x\ ,\ 2x\ ...\ px\ , (p+1)x\ ] [x , 2x ... px ,(p+1)x ]

[ n − 1   ,   n − 1 − x   . . .   n − 1 − p x   ,   n − 1 − ( p + 1 ) x   ] [n-1\ ,\ n-1-x \ ... \ n-1-px\ , \ n-1-(p+1)x\ ] [n1 , n1x ... n1px , n1(p+1)x ]

我们会发现,由 p p p 拓展到 p + 1 p+1 p+1 只需加上 a [ ( p + 1 ) x ] a[(p+1)x] a[(p+1)x] a [ n − 1 − ( p + 1 ) x ] a[n-1-(p+1)x] a[n1(p+1)x]

只需要在拓展的过程中不断取 max 即可;

最终复杂度为 n 1 + n 2 + n 3 . . . . . . n n ≈ n   l o g n \frac{n}{1}+\frac{n}{2}+\frac{n}{3}......\frac{n}{n}\approx n \ logn 1n+2n+3n......nnn logn

Code

#include<bits/stdc++.h>
using namespace std ;
int n ;
long long ans , a[100010] ;
int main()
{
	freopen("frogjump.in","r",stdin) ;
	freopen("frogjump.out","w",stdout) ;
	scanf("%d" , &n ) ;
	for(int i = 0 ; i < n ; i ++ ) {
		scanf("%lld" , &a[i] ) ;
	}
	long long res = 0 ;
	for(int i = 1 ; i < n ; i ++ ) {
		res = 0 ;
		for(int p = 0 ; p < n ; j += p ) {
			int k = n-1-p ;
			if( k % i == 0 && k <= j ) break ; // 跳重复 
			if( k <= i ) break ; // 注意 B < 0  不可取
			res += a[p] + a[k] ;
			ans = max( ans , res ) ; // 结果随p的不同,可能有很多种,取max 
		}
	}
	cout << ans ; 
	return 0 ;
}

最重要的还是性质,考虑怎么减少要枚举的变量

T2

在这里插入图片描述
在这里插入图片描述

题目中对于两点间的 “距离”,存在两个限制:

路径长度补油次数

而要求的答案与二者都有关( m a x v a l < = L max_{val} <= L maxval<=L 且 补油次数最少 )

那么对于这种 路径有多个权值 的问题,我们就可以考虑 多次建图

  1.  处理多源最短路 — — 对于距离小于等于 L L L 的点对 ( x , y ) (x,y) (x,y) ,我们可以将 ( x , y ) (x,y) (x,y) 连一条权值为 1 1 1 的边,代表一次加油次数

  2.  在新的图上跑一遍多源最短路,求出的即是任意两点间最少加油次数

需要注意一点是 起点不用加油,最终答案要 − 1 -1 1


Code

#include<bits/stdc++.h> 
using namespace std ;
const int N = 310 ;
int n , m , l , Q ;
long long dp_dis[N][N] ;
int dp_oil[N][N] ;
int main()
{
    scanf("%d%d%d" , &n , &m , &l ) ;
    int x , y ;
	long long val ;
    memset( dp_dis , 0x3f , sizeof dp_dis ) ;
    memset( dp_oil , 0x3f , sizeof dp_oil ) ;
    for(int i = 1 ; i <= m ; i ++ ) {
    	scanf("%d%d%lld" , &x , &y , &val ) ;
    	dp_dis[x][y] = min( dp_dis[x][y] , val ) ;
    	dp_dis[y][x] = dp_dis[x][y] ;
	}
	for(int k = 1 ; k <= n ; k ++ ) { // 用编号不超过 k 的节点更新 
		for(int i = 1 ; i <= n ; i ++ ) {
			for(int j = i + 1 ; j <= n ; j ++ ) {
				dp_dis[i][j] = min( dp_dis[i][j] , dp_dis[i][k] + dp_dis[k][j] ) ;
				dp_dis[j][i] = dp_dis[i][j] ;
				if( k == n ) {
					if( dp_dis[i][j] <= l ) {
						dp_oil[i][j] = dp_oil[j][i] = 1 ;
					}
				}
			}
		}
	}
	for(int k = 1 ; k <= n ; k ++ ) {
		for(int i = 1 ; i <= n ; i ++ ) {
			for(int j = i + 1 ; j <= n ; j ++ ) {
				dp_oil[i][j] = min( dp_oil[i][j] , dp_oil[i][k] + dp_oil[k][j] ) ;
				dp_oil[j][i] = dp_oil[i][j] ;
			}
		}
	}
	scanf("%d" , &Q ) ;
	while( Q -- ) {
		int s , t ;
		scanf("%d%d" , &s , &t ) ;
		if( dp_oil[s][t] != 0x3f3f3f3f ) printf("%d\n" , dp_oil[s][t] - 1 ) ;
		else printf("-1\n") ;
	}
	return 0 ;
}

一些常用技巧很重要:多权值的路径要考虑多次建图!!!

然而本题还有另一种思路:

我们来考虑两种权值之间的联系

显然,只有当补油次数相同时,路径长度 即消耗的油量 才有意义( 否则消耗一次补油次数肯定会使答案更优 )

也就是说:两种权值存在优先级

那么只需以 补油次数 为第一优先级,到当前节点剩余的油量 为第二优先级,跑正常的最短路算法即可

Code

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

const int N = 310;
const int M = N * N / 2;
int n, m, l, f[N][N], dis[N], oil[N], u, v, w, head[N], tot, s, t, Q;
bool vis[N];
struct edge{
	int v, last, w;
}E[M * 2];

void add(int u, int v, int w){
	E[++tot].v = v;
	E[tot].last = head[u];
	head[u] = tot;
	E[tot].w = w;
}

struct node{ // 到第 i 个点,补油次数,当前油量
	int cnt, oil, id;
	bool operator < (const node &a)const{
		return ((a.cnt < cnt) || (a.cnt == cnt && a.oil > oil));
	}
};

priority_queue< node > q;
void dijkstra(int s){
	memset(dis, 0x3f, sizeof(dis));
	memset(oil, 0xcf, sizeof(oil));
	memset(vis, 0, sizeof(vis));
	
	dis[s] = 0, oil[s] = l;
	q.push(node{dis[s], oil[s], s});
	
	while(!q.empty()){
		node F = q.top();
		q.pop();
		if(vis[F.id]) continue;
		vis[F.id] = 1;
		int cnt = F.cnt, Oil = F.oil, id = F.id;
		for(int i = head[id]; i; i = E[i].last){
			int v = E[i].v;
			if(Oil >= E[i].w){ // 当前剩余油量大于路径长度
				if(dis[v] > cnt) dis[v] = cnt, oil[v] = Oil - E[i].w, q.push(node{dis[v], oil[v], v});
				else if(dis[v] == cnt && Oil - E[i].w > oil[v]) oil[v] = Oil - E[i].w, q.push(node{dis[v], oil[v], v});
			}
			else if(E[i].w <= l){ // 否则在路径长度小于等于 L 的基础上,消耗一次补油次数
				if(dis[v] > cnt + 1) dis[v] = cnt + 1, oil[v] = l - E[i].w, q.push(node{dis[v], oil[v], v});
				else if(dis[v] == cnt + 1 && l - E[i].w > oil[v]) oil[v] = l - E[i].w, q.push(node{dis[v], oil[v], v});
			}
		}
	}
	
	for(int i = 1; i <= n; i++) f[s][i] = dis[i];
	
}

int main(){
	
	scanf("%d%d%d", &n, &m, &l);
	for(int i = 1; i <= m; i++){
		scanf("%d%d%d", &u, &v, &w);
		add(u, v, w);
		add(v, u, w);
	}
	
	for(int i = 1; i <= n; i++){
		dijkstra(i); // dj 跑多源最短路
	}
	
	scanf("%d", &Q);
	for(int i = 1; i <= Q; i++){
        scanf("%d%d", &s, &t);
		if(f[s][t] == 0x3f3f3f3f) printf("-1\n");
		else printf("%d\n", f[s][t]);
	}
	
	return 0;
}
// written by czl dalao %%%

一道同样的多次建图的题目,但不同的一点是路径权值似乎不存在优先级了:长度 与 时间 都需要考虑 ,且无法同时满足

需要使用第一种思想

T6

在这里插入图片描述
在这里插入图片描述
一番思考后,发现常规的算法以及 g c d gcd gcd 的推导并不能很好的解决这个问题

那么结合 N < = 500 N<=500 N<=500 ,我们回归朴素:枚举

假设当前枚举到公因数 d d d ,对于每个 A i A_i Ai ,要么减要么增,使得其变成 d d d 的倍数

等价于对 d d d 取模后,将余数 减到 0 0 0 或 加到 d d d

为了使操作次数尽可能少,当然要对接近 0 0 0 的减,接近 d d d 的增

那么只需将余数排序,贪心的将序列两端最大与最小值结合,一加一减即可

这样判断一个 d d d 是否合法的时间复杂度为 O ( n   l o g n ) O(n\ logn) O(n logn)

如何减少 d d d 的枚举次数?

回归题目,我们会发现一个很有用的性质

不管多少次操作后,序列各项之和不变

那么经过若干次操作后的 s u m sum sum 也必然是 d d d 的倍数

所以只需枚举 s u m sum sum 的所有因数,判断,总时间复杂度 O ( n   l o g n   500 ∗ 1 0 6 ) O(n \ logn \ \sqrt{500*10^6}) O(n logn 500106 )

Code

#include<bits/stdc++.h>  
using namespace std ;
int n , k , sum , ans = 1 ;
int a[510] , minn[510] ;
void test(int i )
{
	for(int j = 1 ; j <= n ; j ++ ) {
		minn[j] = a[j]%i ;
	}
	sort( minn , minn+n+1 ) ;
	int res = 0 , l = 0 , r = n ;
	while( minn[++l] == 0 ) ;
	while( l < r ) {		
		if( minn[l] < i-minn[r] ) res += minn[l] , minn[r] += minn[l] , minn[l] = 0 , l ++ ;
		else {
			res += i-minn[r] ,  minn[l] -= i-minn[r] , minn[r] = 0 , r -- ;
			if( minn[l] == 0 ) l ++ ;
		}
	}
	if( !minn[l] ) {
		if( res <= k ) {
			ans = max( ans , i ) ;
			return ;
 		}
	}
	return ;
}
int main()
{
    scanf("%d%d" , &n , &k ) ;
    for(int i = 1 ; i <= n ; i ++ ) {
    	scanf("%d" , &a[i] ) ;
    	sum += a[i] ;
	}
	int l = sqrt( sum ) ;
	for(int i = 1 ; i <= l ; i ++ ) {
		if( sum % i == 0 ) {
			test( i ) ;
			test( sum/i ) ;
		}
	}
	cout << ans << endl ;
	return 0 ;
}

数据范围较小 且 不易直接求解的 最优解问题,可以通过 枚举答案 转化为 判断类问题

枚举的同时要思考性质,减少枚举次数

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值