第一次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 , A−B ,A−B+A , 2∗(A−B), 2∗(A−B)+A .....分奇偶来看:
令
A
−
B
=
x
A-B = x
A−B=x
偶数步显然为:
[
x
,
2
x
,
3
x
.
.
.
.
.
.
]
[\ x\ , \ 2x\ , 3x\ ......]
[ x , 2x ,3x ......]
奇数步看起来很难讨论
A
A
A 的取值,那么不妨反过来看
只有奇数步才能跳到终点 n − 1 n-1 n−1 ( 往前跳时才能跳到 n − 1 n-1 n−1 )
那么奇数步: [ . . . . . . n − 1 − 2 x , n − 1 − x , n − 1 ] [\ ...... \ \ n-1-2x \ , \ n-1-x \ ,\ n-1 \ ] [ ...... n−1−2x , n−1−x , n−1 ]
所以我们只需要枚举 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\ ] [n−1 , n−1−x ... n−1−px ]
那么下一步
[ 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\ ] [n−1 , n−1−x ... n−1−px , n−1−(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[n−1−(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......nn≈n 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 且 补油次数最少 )
那么对于这种 路径有多个权值 的问题,我们就可以考虑 多次建图
-
处理多源最短路 — — 对于距离小于等于 L L L 的点对 ( x , y ) (x,y) (x,y) ,我们可以将 ( x , y ) (x,y) (x,y) 连一条权值为 1 1 1 的边,代表一次加油次数
-
在新的图上跑一遍多源最短路,求出的即是任意两点间最少加油次数
需要注意一点是 起点不用加油,最终答案要 − 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 500∗106)
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 ;
}
数据范围较小 且 不易直接求解的 最优解问题,可以通过 枚举答案 转化为 判断类问题
枚举的同时要思考性质,减少枚举次数