赛前总结

前言:今天没有考试,就随便总(luan)结(gao)一(yi)下(tong)吧。
注意:本文以总结为主,不会过于详细。

总结类型一:不好发现的暴力条件

在这一类型中,我想总结一下那些正解是找对方向的暴力的题,表面上能暴力过的原因是数据水,实际上是因为题目中有特殊的限制条件,暗示我们只要按照那个方向暴力可过。这种题不太好找,我目前只发现了两道。不过,一旦碰到这种题,有经验的人轻松过,而没经验的人往往有爆零的危险,还是要注意一下的。

算不出来

(由于资料的散失,背景咕咕咕了)
题目大意:给定2n个数,前n个为 a 1 a_1 a1 a n a_n an,后n个为 b 1 b_1 b1 b n b_n bn,n最大为 1 0 6 10^6 106 a i , b i a_i,b_i ai,bi小于 3 ∗ 1 0 6 3*10^6 3106,算出式子 ∑ i = 1 , j = 1 n ⌊ ∣ a i − b j ∣ ⌋ \sum_{i=1,j=1}^n\lfloor \sqrt{|a_i-b_j|} \rfloor i=1,j=1naibj 的值,其中 ∑ i = 1 n a i ≤ 1 0 7 \sum_{i=1}^na_i\le10^7 i=1nai107 ∑ i = 1 n b i ≤ 1 0 7 \sum_{i=1}^nb_i\le10^7 i=1nbi107

简要思路:这题我忽略了后面这一至关重要的限制条件,一直以为有什么高级算法可以优化到 O ( n l o g n ) O(nlogn) O(nlogn)。实际上,这题对所有数的和的大小的限制是最好的提示了,这告诉我们数列 a a a b b b的不同的数绝对到不了 2 ∗ 1 0 7 2*\sqrt{10^7} 2107 个,根据等差数列的求和公式 S = n ∗ ( n + 1 ) 2 S={n*(n+1) \over 2} S=2n(n+1)可知,即使 a a a的所有的数从1开始逐个加一,不同的数达不到 2 ∗ 1 0 7 2*\sqrt{10^7} 2107 个。正确的暴力方向是,用数组判重,统计相同的数的个数,统计答案时将所有相同数的结果一起计算,暴力求解,时间复杂度最多是 O ( 4 ∗ 1 0 7 ) O(4*10^7) O(4107),完全可过。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <cmath>
#include <vector>
#define ll long long
#define N 1000005
using namespace std;
int n;
vector<int> numa , numb;
int a[N] , b[N] , cnta[3 * N] , cntb[3 * N];
ll sum;
template <typename T>
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
int main () {
	//freopen("math.in","r",stdin);
	//freopen("math.out","w",stdout);
	read(n);
	sum = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		read(a[i]);
		if ( !cnta[a[i]] ) {
			numa.push_back(a[i]);
		}
		cnta[a[i]]++;
	}
	for ( int i = 1 ; i <= n ; ++i ) {
		read(b[i]);
		if ( !cntb[b[i]] ) {
			numb.push_back(b[i]);
		}
		cntb[b[i]]++;
	}
	for ( int i = 0 ; i < numa.size() ; ++i ) {
		for ( int j = 0 ; j < numb.size() ; ++j ) {
			sum += (ll)(cnta[numa[i]]) * (ll)(cntb[numb[j]]) * (ll)(sqrt(abs( numa[i] - numb[j] )));
		}
	}
	printf("%lld",sum);
	return 0;
}

Fibonacci-ish

题面(来自洛谷):Yash最近迷上了fibonacci数列,他定义了一种数列叫fibonacccccci数列:
1. 这个数列包含至少2个元素;
2. f[0]和f[1]是任意选取的;
3. f[n+2]=f[n+1]+f[n] (n>=0);
现在,给出一个数列a[1…n],你可以改变数列元素的顺序,使得a[1…m] 满足fibonacccccci数列的条件,请求出最大的m。其中2<=n<=1000 , ∣ a i ∣ < = 1 0 9 ∣a_i∣<=10^9 ai<=109

简要思路:一开始,我总认为这种暴力非常恐怖,不过根据以下打表程序,发现 1 0 9 10^9 109以内的斐波那契数列只有45项。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
using namespace std;
const int end = 1e9;
ll f[1005];
int main () {
	f[1] = 1;
	f[2] = 1;
	for ( int i = 3 ; ; ++i ) {
		f[i] = f[i - 1] + f[i - 2];
		if ( f[i] > end ) {
			cout << i << endl;
			cout << f[i];
			return 0;
		}
	}
	return 0;
}

算上负数,也不过多了几项罢了。
直接暴力枚举就行了。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <map>
using namespace std;
int n;
int f[515];
int a[1005];
map< int , int > cnt;
template< typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
int main () {
	read(n);
	for ( int i = 1 ; i <= n ; ++i ) {
		read(a[i]);
		++cnt[a[i]];
	}
	int ans = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		for ( int j = 1 ; j <= n ; ++j ) {
			if ( i == j ) {
				continue;
			}
			if ( (!a[i]) && (!a[j]) ) {
				if ( ans < cnt[0] ) {
					ans = cnt[0];
				}
				continue;
			}
			f[1] = a[i];
			f[2] = a[j];
			int f1 = a[i];
			int f2 = a[j];
			cnt[f1]--;
			cnt[f2]--;
			int tim = 2;
			int now;
			while( cnt[now = f1 + f2] > 0 ) {
				f[++tim] = now;
				f1 = f2;
				f2 = now;
				cnt[now]--;
			}
			if ( tim > ans ) {
				ans = tim;
			}
			for ( int k = 1 ; k <= tim ; ++k ) {
				cnt[f[k]]++;
			}
		}
	}
	printf("%d",ans);
	return 0;
}

总结类型二:开始流行的树上操作难题

通过对往年真题的训练,不难发现,图论占了非常大的一个份额。每年几乎必有图论来压一压轴。学会正确分析图论题的特点和突破口是拿高分(AK)的关键。
我先从简单的开始吧。

货车运输

NOIP提高组2013原题
简要思路:这题我一开始作死的用了二分答案,加上强连通分量判断,只拿了三十分,后面加上并查集,也只有六十分,直到后面才发现,这题不过只是最大生成树加上LCA,用f处理节点,g处理路径上的最小边罢了。那么,我怎么才能想到这一点呢?我们可亲可敬的教练ZHC如是说:

考虑到只有途经的最小的边会影响答案,若要使答案最优(最大),要去除一些无用的边来减小无用的搜索开销。为了去掉边权小的边并且保证原先在图中联通的点继续联通,考虑最大生成树

接下来就是细节问题了,直接看代码吧。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 10005
using namespace std;
int n , m , bcnt , q , ans;
int head[N];
struct no{
	int next;
	int to;
	int val;
}str[2 * N];
int f[N][31] , g[N][31] , fa[N] , loger[N] , height[N];
struct node{
	int x;
	int y;
	int val;
}sr[5 * N];
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline void insrt( int from , int to , int val ) {
	str[++bcnt].next = head[from];
	head[from] = bcnt;
	str[bcnt].to = to;
	str[bcnt].val = val;
	return;
}
inline int find( int x ) {
	if ( x == fa[x] ) {
		return x;
	} else {
		fa[x] = find( fa[x] );
		return fa[x];
	}
}
inline void onion( int x , int y ) {
	int fx = find(x);
	int fy = find(y);
	if ( fx != fy ) {
		fa[fx] = fy;
	}
	return;
}
inline bool cmp( node a , node b ) {
	return a.val > b.val;
}
inline void dfs( int cur , int fa ) {
	height[cur] = height[fa] + 1;
	for ( int i = 1 ; i <= loger[height[cur]] ; ++i ) {
		f[cur][i] = f[f[cur][i - 1]][i - 1];
		g[cur][i] = min( g[cur][i - 1] , g[f[cur][i - 1]][i - 1] );
	}
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa ) {
			continue;
		}
		f[sn][0] = cur;
		g[sn][0] = str[i].val;
		dfs( sn , cur );
	}
	return;
}
inline void lca( int x , int y ) {
	if ( height[x] < height[y] ) {
		x ^= y;
		y ^= x;
		x ^= y;
	}
	ans = 1000000000;
	for ( int i = loger[height[x]] ; i >= 0 ; --i ) {
		if ( height[f[x][i]] >= height[y] ) {
			ans = min( ans , g[x][i] );
			x = f[x][i];
		}
	}
	if ( x == y ) {
		return;
	}
	for ( int i = loger[height[x]] ; i >= 0 ; --i ) {
		if ( f[x][i] != f[y][i] ) {
			ans = min( ans , min( g[x][i] , g[y][i] ) );
			x = f[x][i];
			y = f[y][i];
		}
	}
	ans = min( ans , min( g[x][0] , g[y][0] ) );
	return;
}
int main () {
	//freopen( "truck.in" , "r" , stdin );
	//freopen( "truck.out" , "w" , stdout );
	read(n);
	read(m);
	loger[0] = -1;
	for ( int i = 1 ; i <= n ; ++i ) {
		loger[i] = loger[i >> 1] + 1;
	}
	for ( int i = 1 ; i <= m ; ++i ) {
		read(sr[i].x);
		read(sr[i].y);
		read(sr[i].val);
	}
	for ( int i = 1 ; i <= n ; ++i ) {
		fa[i] = i;
	}
	sort( sr + 1 , sr + 1 + m , cmp );
	for ( int i = 1 ; i <= m ; ++i ) {
		int xx = sr[i].x;
		int yy = sr[i].y;
		int fx = find(xx);
		int fy = find(yy);
		if ( fx != fy ) {
			insrt( xx , yy , sr[i].val );
			insrt( yy , xx , sr[i].val );
			onion( xx , yy );
		}
	}
	g[1][0] = 0;
	f[1][0] = 0;
	height[0] = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		if ( !height[i] ) {
			dfs( i , 0 );
		}
	}
	read(q);
	int x , y;
	for ( int i = 1 ; i <= q ; ++i ) {
		read(x);
		read(y);
		if ( find(x) != find(y) ) {
			printf("-1\n");
			continue;
		}
		lca( x , y );
		printf("%d\n",ans);
	}
	return 0;
}

通过这题不难发现,学会尽可能去除无用状态是解题的关键。

树网的核

NOIP提高组2007原题
原题数据为 5 ≤ n ≤ 300 5\le n\le300 5n300 0 ≤ s ≤ 1000 0\le s \le 1000 0s1000
BZOJ上加强版为 n ≤ 5 ∗ 1 0 5 n \le 5 * 10^5 n5105
这里我们讨论数据加强版的。
当数据加强时,这题瞬间从一道暴力水题变成经典图论难题。
简要思路:题目已经告诉我们,树的直径不唯一,但是必定全部相交,并且中点汇聚于一处。
这可用反证法导出矛盾来证明。
同时我们不难得出在任意一条直径上求出的最小偏心距都相等,否则,可以构造出更长的直径,与题意矛盾。
好吧(╯▽╰)我承认是lyd的,那我直接跳过前面的解法吧 )。
设在直径上的节点为 u 1 , u 2 , ⋯   , u t u_1,u_2,\cdots,u_t u1,u2,,ut共t个节点,我们先把这几个节点标记为已访问,然后每个点进行一个dfs,求出 d [ u i ] d[u_i] d[ui]表示从点 u i u_i ui不经过同一直径上的点,可以到达的最远的点的距离.。
则以 u i , u j ( i ≤ j , d i s ( u i , u j ) ≤ s ) u_i,u_j(i \le j , dis( u_i , u_j) \le s ) ui,uj(ij,dis(ui,uj)s)为端点的数网的偏心距就是: m a x ( m a x i ≤ k ≤ j { d [ u k ] } , d i s ( u 1 , u i ) , d i s ( u j , u t ) ) max(max_{i \le k \le j}\{ d[u_k] \} , dis( u_1,u_i ) , dis( u_j,u_t)) max(maxikj{d[uk]},dis(u1,ui),dis(uj,ut)),此时我们只是简单的用数学方法来表示答案,就发现这可用单调队列优化出一个 O ( n ) O(n) O(n)的算法,后面两个dis用指针即可。
不过,根据直径的最长性,我们可以去掉单调队列,直接用式子: m a x ( m a x 1 ≤ k ≤ t { d [ u k ] } , d i s ( u 1 , u i ) , d i s ( u j , u t ) ) max(max_{1 \le k \le t}\{ d[u_k] \} , dis( u_1,u_i ) , dis( u_j,u_t)) max(max1kt{d[uk]},dis(u1,ui),dis(uj,ut))来得出答案。
那么为什么我们可以直接用树的直径的偏心距来更新答案呢?
答案也不困难,如果设取得偏心距的点为 u k u_k uk,且 k < i k < i k<i k > j k > j k>j,且偏心距大于树的核心到两端的距离,那么将 u k u_k uk以及离它最远的点所构成的链取代原先 u k u_k uk所靠近的一端,将得到更长的直径,与定力矛盾。
所以,本题只要三遍dfs即可过BZOJ上的数据,不过,acwing上的还是过不了。
原因很简单,爆栈啊!
将dfs改为bfs即可,如果不习惯,可以先写dfs再注释掉,对照改为bfs。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue> 
using namespace std;
const int N = 5e5 + 5;
int n , s , pos , bcnt , maxn , ans;
int head[N] , dis[N] , f[N]/*单项链表*/ , check[N];
struct node{
	int next;
	int to;
	int val;
}str[N * 2];
inline void read( int & res ) {
	res = 0;
	int pd = 1;
	char a = getchar();
	while ( a < '0' || a > '9' ) {
		if ( a == '-' ) {
			pd = -pd;
		}
		a = getchar();
	}
	while ( a >= '0' && a <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( a - '0' );
		a = getchar();
	}
	res *= pd;
	return;
}
inline void insert( int from , int to , int val ) {
	str[++bcnt].next = head[from];
	head[from] = bcnt;
	str[bcnt].to = to;
	str[bcnt].val = val;
	return;
}
inline int max( int a , int b ) {
	return a > b ? a : b;
}
inline int min( int a , int b ) {
	return a < b ? a : b;
}
inline void bfs( int cu , int fa ) {
	f[cu] = fa;
	/*if ( dis[cur] > maxn ) {
		maxn = dis[cur];

		pos = cur;
	}
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa || check[sn] ) {
			continue;
		}
		dis[sn] = dis[cur] + str[i].val;
		dfs( sn , cur );
	}
	return;*/
	//dfs转bfs,不然会爆栈 
	queue<int> q;
	q.push(cu);
	while ( !q.empty() ) {
		int cur = q.front();
		q.pop();
		if ( dis[cur] > maxn ) {
			maxn = dis[cur];
			pos = cur;
		}
		for ( int i = head[cur] ; i ; i = str[i].next ) {
			int sn = str[i].to;
			if ( sn == f[cur] || check[sn] ) {
				continue;
			}
			dis[sn] = dis[cur] + str[i].val;
			f[sn] = cur;
			q.push(sn);
		}
	}
}
int main () {
	read(n);
	read(s);
	int x , y , z;
	for ( int i = 1 ; i <= n - 1 ; ++i ) {
		read(x);
		read(y);
		read(z);
		insert( x , y , z );
		insert( y , x , z );
	}
	maxn = 0;
	dis[1] = 0;
	bfs( 1 , 0 );
	dis[pos] = 0;
	int tem = pos;
	maxn = 0;
	bfs( tem , 0 );
	ans = 0x7f7f7f7f;
	tem = pos;
	for ( int i = tem , j = tem ; i ; i = f[i] ) {
		while ( dis[j] - dis[i] > s ) {
			j = f[j];
		}
		ans = min( ans , max( dis[pos] - dis[j] , dis[i] ) );
	}
	for ( int i = tem ; i ; i = f[i] ) {
		check[i] = 1;
	}
	for ( int i = tem ; i ; i = f[i] ) {
		pos = i;
		dis[pos] = 0;
		bfs( i , f[i] );
	}
	int res = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		res = max( res , dis[i] );
	}
	ans = max( ans , res );
	printf("%d",ans);
	return 0;
}

通过这题不难发现,将题目所求的东西转化为严谨的数学公式会带来不少优化的方向。

疫情控制

NOIP提高组2012原题
简要思路:这题是一道思路不是太难想但是代码实现非常困难的题,思路很简单,就是采用二分法来求出最少时间。至于判断嘛,就比较麻烦。简单思路就是将所有军队尽可能地移向根节点,如果到不了根节点,就在自己能到达的深度最浅的节点驻扎下来。至于那些能到达根节点的军队,先保存下他们越过根节点后剩余的时间。这里有一个引理,如果当前军队越过根节点后,剩下时间不足以返回原处,并且原处所属子树未有军队驻扎,那么这支军队显然留下来更好。最后,一个数组存可利用军队,另一个存需驻扎子树,两两配对即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 50005
#define ll long long
using namespace std;
int n , m , bcnt;
int head[N] , loger[N] , armypos[N] , f[N][31] , depth[N] , sta[N] , rest[N] , ineed[N];
ll g[N][31] , tim[N] , need[N];
ll l , r;
pair< ll , int > army[N];
struct node{
	int next;
	int to;
	ll val;
}str[N * 2];
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa- '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline void insert( int from , int to , ll val ) {
	str[++bcnt].next = head[from];
	str[bcnt].to = to;
	str[bcnt].val = val;
	head[from] = bcnt;
	return;
}
inline ll max( ll a , ll b ) {
	return a > b ? a : b;
}
inline void dfs1( int cur , int fa ) {
	depth[cur] = depth[fa] + 1;
	//cout << cur << endl;
	//cout << f[cur][0] << " " << g[cur][0] << endl;
	for ( int i = 1 ; i <= loger[depth[cur]] + 1 ; ++i ) {
		f[cur][i] = f[f[cur][i - 1]][i - 1];
		g[cur][i] = g[cur][i - 1] + g[f[cur][i - 1]][i - 1];
		//r = max( r , g[cur][i] );
		//cout << f[cur][i] << " " << g[cur][i] << endl;
	}
	r += g[cur][loger[depth[cur]] + 1];
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa ) {
			continue;
		} 
		f[sn][0] = cur;
		g[sn][0] = str[i].val;
		dfs1( sn , cur );
	}
	return;
}
inline int dfs2( int cur , int fa ) {
	if ( sta[cur] ) {
		return 1;
	}
	int scnt = 0;
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa ) {
			continue;
		}
		scnt++;
		if ( !dfs2( sn , cur ) ) {
			return 0;
		}
	}
	if ( !scnt ) {
		return 0;
	} else {
		return 1;
	}
}
inline int check( ll maxnlen ) {
	memset( sta , 0 , sizeof(sta) );
	int acnt = 0;
	for ( int i = 1 ; i <= m ; ++i ) {
		int id = armypos[i];
		ll cnt = 0;
		for ( int j = loger[depth[id]] + 1 ; j >= 0 ; --j ) {
			if ( f[id][j] > 1 && g[id][j] + cnt <= maxnlen ) {
				cnt += g[id][j];
				id = f[id][j];
			}
		}
		if ( f[id][0] == 1 && cnt + g[id][0] <= maxnlen ) {
			army[++acnt] = make_pair( maxnlen - cnt - g[id][0] , id );
		} else {
			sta[id] = 1;
		}
	}
	for ( int i = head[1] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( !dfs2( sn , 1 ) ) {
			ineed[sn] = 1;
		} else {
			ineed[sn] = 0;
		}
	}
	sort( army + 1 , army + 1 + acnt );
	int atot = 0;
	int ntot = 0;
	for ( int i = 1 ; i <= acnt ; ++i ) {
		if ( ineed[army[i].second] && army[i].first < g[army[i].second][0] ) {//引理
			ineed[army[i].second] = 0;
		} else {
			tim[++atot] = army[i].first;
		}
	}
	for ( int i = head[1] ; i ; i = str[i].next ) {
		if ( ineed[str[i].to] ) {
			need[++ntot] = g[str[i].to][0];
		}
	}
	if ( atot < ntot ) {
		return 0;
	}
	sort( need + 1 , need + 1 + ntot );
	int i = 1 , j = 1;
	while ( i <= ntot && j <= atot ) {//两两配对
		if( need[i] <= tim[j] ) {
			i++;
			j++;
		} else {
			j++;
		}
	}
	if ( i >= ntot + 1 ) {
		return 1;
	} else {
		return 0;
	}
}
int main () {
	read(n);
	int u , v;
	ll w;
	for ( int i = 1 ; i <= n - 1 ; ++i ) {
		read(u);
		read(v);
		read(w);
		insert( u , v , w );
		insert( v , u , w );
	}
	read(m);
	for ( int i = 1 ; i <= m ; ++i ) {
		read( armypos[i] );
	}
	loger[0] = -1;
	for ( int i = 1 ; i <= n ; ++i ) {
		loger[i] = loger[i >> 1] + 1;
	}
	r = 0;
	dfs1( 1 , 0 );
	l = 0;
	int flag = 0;
	while ( l < r ) {
		ll mid = ( l + r ) >> 1;
		if ( check( mid ) ) {
			r = mid;
			flag = 1;
		} else {
			l = mid + 1;
		}
	}
	if ( !flag ) {
		printf("-1\n");
	} else {
		printf("%lld",r);
	}
	return 0;
}

NOIP提高组2015原题

运输计划

NOIP提高组2015原题
简要思路:实际上思维难度非常低,还是用二分的方法(我知道有的大佬不用二分,但是我的level匹配不来那些巨佬的高级方法 )。判断让每条路径不超过某一值是否可行,可先将所有的不符合条件的路径保存下来,并记录最长路径,然后贪心地选最长公共边即可,如果最长路径减去最长公共边还是大于给定的值,说明这个限制条件不行;反之,证明它可行。至于判断一个边是否为公共边,可用树上差分的方法计算通过这条边的路径数来判断。详见代码吧。

#include <iostream>
#include <cstdio>
#include <cstring>
#define N 300005
#define ll long long
using namespace std;
int n , m , bcnt , bbcnt , maxcut , maxn , scnt;
struct node1{
	int next;
	int to;
	int val;
}str[2 * N];
struct node2{
	int next;
	int to;
}strr[2 * N];
int head[N] , hhead[N] , f[N] , dis[N] , vis[N] , sum[N]/*树上差分计数*/ , len[N]/*该点以上的边的大小*/;
struct Query{
	int x , y;
	int lca;
	int len;
}q[N];
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline void insert( int from , int to , int val ) {
	str[++bcnt].next = head[from];
	head[from] = bcnt;
	str[bcnt].to = to;
	str[bcnt].val = val;
	return;
}
inline void iinsert( int from , int to ) {
	strr[++bbcnt].next = hhead[from];
	hhead[from] = bbcnt;
	strr[bbcnt].to = to;
	return;
}
inline int find( int x ) {
	if ( x == f[x] ) {
		return x;
	} else {
		return f[x] = find( f[x] );
	}
}
inline void tarjan( int cur , int fa ) {
	vis[cur] = 1;
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa ) {
			continue;
		}
		dis[sn] = dis[cur] + str[i].val;
		len[sn] = str[i].val;
		tarjan( sn , cur );
		f[sn] = cur;
	}
	for ( int i = hhead[cur] ; i ; i = strr[i].next ) {
		int sn = strr[i].to;
		if ( !vis[sn] ) {
			continue;
		}
		q[i >> 1].lca = find( sn );
		q[i >> 1].len = dis[cur] + dis[sn] - 2 * dis[q[i >> 1].lca];
	}
	return;
}
inline void dfs( int cur , int fa ) {
	for ( int i = head[cur] ; i ; i = str[i].next ) {
		int sn = str[i].to;
		if ( sn == fa ) {
			continue;
		}
		dfs( sn , cur );
		sum[cur] += sum[sn];
	}
	if ( sum[cur] == scnt && len[cur] > maxcut ) {
		maxcut = len[cur];
	}
	return;
}
inline bool check( int lim ) {
	memset( sum , 0 , sizeof(sum) );
	scnt = 0;
	for ( int i = 1 ; i <= m ; ++i ) {
		if ( q[i].len > lim ) {
			sum[q[i].x]++;
			sum[q[i].y]++;
			sum[q[i].lca] -= 2;
			scnt++;
		}
	}
	maxcut = 0;
	dfs( 1 , 0 );
	if ( maxn - maxcut <= lim ) {
		return true;
	} else {
		return false;
	}
}
int main () {
	//freopen( "transport.in" , "r" , stdin );
	//freopen( "transport.out" , "w" , stdout );
	read(n);
	read(m);
	bcnt = 0;
	bbcnt = 1;
	int u , v , w;
	for ( int i = 1 ; i <= n ; ++i ) {
		f[i] = i;
	}
	for ( int i = 1 ; i <= n - 1 ; ++i ) {
		read(u);
		read(v);
		read(w);
		insert( u , v , w );
		insert( v , u , w );
	}
	for ( int i = 1 ; i <= m ; ++i ) {
		read(q[i].x);
		read(q[i].y);
		iinsert( q[i].x , q[i].y );
		iinsert( q[i].y , q[i].x );
	}
	dis[1] = 0;
	tarjan( 1 , 0 );
	maxn = 0;
	for ( int i = 1 ; i <= m ; ++i ) {
		if ( q[i].len > maxn ) {
			maxn = q[i].len;
		}
	}
	int l = 0;
	int r = maxn;
	while ( l < r ) {
		int mid = ( l + r ) >> 1;
		if ( check( mid ) ) {
			r = mid;
		} else {
			l = mid + 1;
		}
	}
	printf("%d",r);
	return 0;
}

通过以上两题不难发现,有时候图论题想到不一定写得出来,还是要巩固一下树上的基本操作等知识点,如树上倍增,树上差分,树的直径求法以及相关性质等。

接下来,就到了这一环节的boss了。我打算将著名神题“天天爱跑步”总结一下,不过,为了更好的理解,我先讲一道与它相关的题“雨天的尾巴”

雨天的尾巴

虽然洛谷上有原题,但我还是贴一下吧。
题面
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
有 n 个点,形成一个树状结构。
有 m 次发放操作,每次选择两个点 x,y,对 x 到 y 的路径上(包括 x,y)的每个点发放一袋 z 类型的物品。
求完成所有发放操作后,每个点存放最多的是哪种类型的物品。

输入格式
第一行两个正整数n,m,含义如题目所示。
接下来n-1行,每行两个数(a,b),表示(a,b)间有一条边。
再接下来m行,每行三个数(x,y,z),含义如题目所示。

输出格式
共n行,第i行一个整数,表示第i座房屋里存放的最多的是哪种救济粮,如果有多种救济粮存放次数一样,输出编号最小的。
如果某座房屋里没有救济粮,则对应一行输出0。

数据范围
1≤n,m≤100000,
1≤z≤10^9

样例输入

5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3

样例输出

2
3
3
0
2

简要思路:我之所以喜欢这题,不仅是因为它可以帮我们打开“天天爱跑步”的大门,而且顺带帮本蒟蒻复习了动态开点线段树。
因为只是统计数量最大的某种物品,与物品本身代表类型的值的大小无关,所以我们优先考虑离散化物品的类型。然后对于每一个点 x x x,建立一个大小为 M M M的计数数组为 c [ x ] [ 1 − M ] c[x][1 - M] c[x][1M]。依次处理每次发放操作,这样时间复杂度为 O ( N ∗ M ) O(N*M) O(NM)。显然要优化。
对于树上的路径,很明显可用到LCA,由于是多组离线询问,可用tarjan一次性全部处理。对于每次发放,我们可用树上差分处理,让点 x x x处物品数加上1,点 y y y处物品数加上1,LCA 处减去1,LCA的父节点(如果有)处减去1。当把所有的操作处理完后,可用dfs统计每个答案。
为了优化处理过程,我们可用动态开点线段树来替代那二维数组,用线段树合并来统计子树和,时空复杂度均为 O ( ( N + M ) l o g ( N + M ) ) O((N+M)log(N+M)) O((N+M)log(N+M))
细节见代码吧~~

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 100005
using namespace std;
int n , m;
struct node{
    int next;
    int to;
}str[2 * N] , strr[2 * N];
struct Query{
    int x , y , lca;
}q[N];
struct Segment{
    int pos , cnt , ls , rs;
}seg[N * 4 * 20];
int segcnt;
int root[N] , head[N] , hhead[N] , fa[N] , f[N] , vis[N] , ans[N] , valu[N], z[N];
int bcnt , bbcnt , cnt;
template < typename T >
inline void read( T & res ) {
    res = 0;
    T pd = 1;
    char aa = getchar();
    while ( aa < '0' || aa > '9' ) {
        if ( aa == '-' ) {
            pd = -pd;
        }
        aa = getchar();
    }
    while ( aa >= '0' && aa <= '9' ) {
        res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
        aa = getchar();
    }
    res *= pd;
    return;
}
inline void insert( int from , int to ) {
    str[++bcnt].next = head[from];
    head[from] = bcnt;
    str[bcnt].to = to;
    return;
}
inline void iinsert( int from , int to ) {
    strr[++bbcnt].next = hhead[from];
    hhead[from] = bbcnt;
    strr[bbcnt].to = to;
    return;
}
inline int find( int x ) {
    if ( fa[x] == x ) {
        return x;
    } else {
        return fa[x] = find( fa[x] );
    }
}
inline int max( int a , int b ) {
    return a > b ? a : b;
}
inline void tarjan( int cur , int fat ) {
    vis[cur] = 1;
    fa[cur] = cur;
    f[cur] = fat;
    for ( int i = head[cur] ; i ; i = str[i].next ) {
        int sn = str[i].to;
        if ( sn == fat ) {
            continue;
        }
        tarjan( sn , cur );
        fa[sn] = cur;
    }
    for ( int i = hhead[cur] ; i ; i = strr[i].next ) {
        int sn = strr[i].to;
        if ( !vis[sn] ) {
            continue;
        }
        q[i >> 1].lca = find( sn );
    }
    return;
}
inline void update( int p , int l , int r , int val , int deta ) {
    if ( l == r ) {
        seg[p].cnt += deta;
        seg[p].pos = seg[p].cnt > 0 ? l : 0;
        return;
    }
    int mid = ( l + r ) >> 1;
    if ( val <= mid ) {
        if ( !seg[p].ls ) {
            seg[p].ls = ++segcnt;
        }
        update( seg[p].ls , l , mid , val , deta );
    } else {
        if ( !seg[p].rs ) {
            seg[p].rs = ++segcnt;
        }
        update( seg[p].rs , mid + 1 , r , val , deta );
    }
    seg[p].cnt = max( seg[seg[p].ls].cnt , seg[seg[p].rs].cnt );
    seg[p].pos = seg[seg[p].ls].cnt >= seg[seg[p].rs].cnt ? seg[seg[p].ls].pos : seg[seg[p].rs].pos;
    return;
}
inline int merge( int p , int q , int l , int r ) {
    if ( !p ) {
        return q;
    }
    if ( !q ) {
        return p;
    }
    if ( l == r ) {
        seg[p].cnt += seg[q].cnt;
        seg[p].pos = seg[p].cnt > 0 ? l : 0;
        return p;
    }
    int mid = ( l + r ) >> 1;
    seg[p].ls = merge( seg[p].ls , seg[q].ls , l , mid );
    seg[p].rs = merge( seg[p].rs , seg[q].rs , mid + 1 , r );
    seg[p].cnt = max( seg[seg[p].ls].cnt , seg[seg[p].rs].cnt );
    seg[p].pos = seg[seg[p].ls].cnt >= seg[seg[p].rs].cnt ? seg[seg[p].ls].pos : seg[seg[p].rs].pos;
    return p;
}
inline void dfs( int cur , int fat ) {
    for ( int i = head[cur] ; i ; i = str[i].next ) {
        int sn = str[i].to;
        if ( sn == fat ) {
            continue;
        }
        dfs( sn , cur );
        root[cur] = merge( root[cur] , root[sn] , 1 , cnt );
    }
    ans[cur] = seg[root[cur]].pos;
    return;
}
int main () {
    read(n);
    read(m);
    bcnt = 0;
    bbcnt = 1;
    segcnt = 0;
    int u , v;
    for ( int i = 1 ; i <= n - 1 ; ++i ) {
        read(u);
        read(v);
        insert( u , v );
        insert( v , u );
    }
    for ( int i = 1 ; i <= m ; ++i ) {
        read(q[i].x);
        read(q[i].y);
        read(z[i]);
        valu[i] = z[i];
        iinsert( q[i].x , q[i].y );
        iinsert( q[i].y , q[i].x );
    }
    tarjan( 1 , 0 );
    for ( int i = 1 ; i <= n ; ++i ) {
        root[i] = ++segcnt;
    }
    sort( valu + 1 , valu + 1 + m );
    cnt = unique( valu + 1 , valu + 1 + m ) - valu - 1;//巧妙离散化的方法
    for ( int i = 1 ; i <= m ; ++i ) {
        int val = lower_bound( valu + 1 , valu + 1 + cnt , z[i] ) - valu;
        update( root[q[i].x] , 1 , cnt , val , 1 );
        update( root[q[i].y] , 1 , cnt , val , 1 );
        update( root[q[i].lca] , 1 , cnt , val , -1 );
        if ( f[q[i].lca] ) {
            update( root[f[q[i].lca]] , 1 , cnt , val , -1 );
        }
    }
    dfs( 1 , 0 );
    for ( int i = 1 ; i <= n ; ++i ) {
        printf("%d\n",valu[ans[i]]);
    } 
    return 0;
}

接下来,BOSS到了┌(。Д。)┐。

天天爱跑步

NOIP2016原题
简要思路:每个玩家的跑步路线可以拆成两段:从 s i s_i si l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti),从 l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti) t i t_i ti,其中后面这段不包括 l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti)这一端点。要统计观察员观察到的总玩家数,不如转化一下,统计某个玩家能让那些观察员观察到,这也正好相当于对一次次操作造成影响的统计。思维这样转化一下,题目稍微好做了一点。接下来讨论时, w [ x ] w[x] w[x]代表节点 x x x的观察员出现的时间, d [ x ] d[x] d[x]代表节点 x x x在树中的深度。
要使位于节点 x x x能观察到第 i i i个玩家,考虑刚刚剖分的两条路径,可使该观察员满足一下两种条件之一:
1.点 x x x处于点 s i s_i si l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti)的路径上,并满足式子 d [ s i ] − d [ x ] = w [ x ] d[s_i]-d[x]=w[x] d[si]d[x]=w[x]我们根据直觉,将同类项移到一边,得 d [ x ] + w [ x ] = d [ s i ] d[x]+w[x]=d[s_i] d[x]+w[x]=d[si]这时,再结合上一题的发放物品的思想,不难发现假如我们在 s i s_i si l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti)上放一个值为 d [ s i ] d[s_i] d[si]的物品,对所有玩家(操作)的影响进行统计(树上差分)。最后统计答案时,如果到了某一点 x x x,值为 d [ x ] + w [ x ] d[x]+w[x] d[x]+w[x]的物品有 k k k个,那么在 x x x点的观察员可观察到 k k k个玩家。
2.点 x x x处于点 l c a ( s i , t i ) lca(s_i,t_i) lca(si,ti) t i t_i ti的路径上,并满足式子 d [ s i ] + d [ x ] − 2 ∗ d [ l c a ( s i , t i ) ] = w [ x ] d[s_i]+d[x]-2*d[lca(s_i,t_i)]=w[x] d[si]+d[x]2d[lca(si,ti)]=w[x]同理可得 w [ x ] − d [ x ] = d [ s i ] − 2 ∗ d [ l c a ( s i , t i ) ] w[x]-d[x]=d[s_i]-2*d[lca(s_i,t_i)] w[x]d[x]=d[si]2d[lca(si,ti)]也将物品统计下来,不过由于上式可能出现负数,要平移一下。
由于我们只是统计物品是否存在,并且用上题的线段树合并时间复杂度过高,可用vector来存贮每个点的状况。同时上面两种情况要分开两种动态数组进行统计(防止混了),加减也要分开。最后有两个全局数组统计所有的物品的数目,通过DFS前后差可统计对当前点上的观察员有效的物品总数。
具体细节见代码吧。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <vector> 
#define N 300005
using namespace std;
int n , m , bcnt , bbcnt;
struct node{
    int next;
    int to;
}str[2 * N] , strr[2 * N];
int head[N] , hhead[N] , dis[N] , vis[N] , w[N] , f[N] , ans[N] , fat[N];
int cnt1[N * 2] , cnt2[N * 2];
struct Query{
    int x , y , lca;
}q[N];
vector<int> ladd[N] , lcut[N] , radd[N] , rcut[N];
template < typename T >
inline void read( T & res ) {
    res = 0;
    T pd = 1;
    char aa = getchar();
    while ( aa < '0' || aa > '9' ) {
        if ( aa == '-' ) {
            pd = -pd;
        }
        aa = getchar();
    }
    while ( aa >= '0' && aa <= '9' ) {
        res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
        aa = getchar();  
    }
    res *= pd;
    return;
}
inline void insert( int from , int to ) {
    str[++bcnt].next = head[from];
    head[from] = bcnt;
    str[bcnt].to = to;
    return;
}
inline void iinsert( int from , int to ) {
    strr[++bbcnt].next = hhead[from];
    hhead[from] = bbcnt;
    strr[bbcnt].to = to;
    return;
}
inline int find( int x ) {
    if ( x == f[x] ) {
        return x;
    } else {
        return f[x] = find( f[x] );
    }
}
inline void tarjan( int cur , int fa ) {
    vis[cur] = 1;
    dis[cur] = dis[fa] + 1;
    fat[cur] = fa;
    for ( int i = head[cur] ; i ; i = str[i].next ) {
        int sn = str[i].to;
        if ( sn == fa ) {
            continue;
        }
        tarjan( sn , cur );
        f[sn] = cur;
    }
    for ( int i = hhead[cur] ; i ; i = strr[i].next ) {
        int sn = strr[i].to;
        if ( !vis[sn] ) {
            continue;
        }
        q[i >> 1].lca = find( sn );
    }
    return;
}
inline void dfs( int cur , int fa ) {
    int prex = cnt1[w[cur] + dis[cur]];
    int prey = cnt2[w[cur] - dis[cur] + n];
    for ( int i = head[cur] ; i ; i = str[i].next ) {
        int sn = str[i].to;
        if ( sn == fa ) {
            continue;
        }
        dfs( sn , cur );
    }
    for ( int i = 0 ; i < ladd[cur].size() ; ++i ) {
        cnt1[ladd[cur][i]]++;
    }
    for ( int i = 0 ; i < lcut[cur].size() ; ++i ) {
        cnt1[lcut[cur][i]]--;
    }
    for ( int i = 0 ; i < radd[cur].size() ; ++i ) {
        cnt2[radd[cur][i]]++;
    }
    for ( int i = 0 ; i < rcut[cur].size() ; ++i ) {
        cnt2[rcut[cur][i]]--;
    }
    ans[cur] = cnt1[w[cur] + dis[cur]] - prex + cnt2[w[cur] - dis[cur] + n] - prey;
    return;
}
int main () {
//  freopen( "running.in" , "r" , stdin );
//  freopen( "running.out" , "w" , stdout );
    read(n);
    read(m);
    bcnt = 0;
    bbcnt = 1;
    int u , v;
    for ( int i = 1 ; i <= n - 1 ; ++i ) {
        read(u);
        read(v);
        insert( u , v );
        insert( v , u );
    }
    for ( int i = 1 ; i <= n ; ++i ) {
        f[i] = i;
        read(w[i]);
    }
    for ( int i = 1 ; i <= m ; ++i ) {
        read(q[i].x);
        read(q[i].y);
        iinsert( q[i].x , q[i].y );
        iinsert( q[i].y , q[i].x );
    }
    tarjan( 1 , 0 );
    for ( int i = 1 ; i <= m ; ++i ) {
        ladd[q[i].x].push_back(dis[q[i].x]);
        lcut[fat[q[i].lca]].push_back(dis[q[i].x]);
        radd[q[i].y].push_back( dis[q[i].x] - 2 * dis[q[i].lca] + n );
        rcut[q[i].lca].push_back( dis[q[i].x] - 2 * dis[q[i].lca] + n );
    }
    dfs( 1 , 0 );
    for ( int i = 1 ; i <= n - 1 ; ++i ) {
        printf("%d ",ans[i]);
    }
    printf("%d",ans[n]);
    return 0;
}

好了,boss搞定了\ ( ^ o ^ ) / ~。

总结类型三:数据结构的妙用

很多数据结构我们手打出来不难,但是一旦题目变得巧妙了,我们只好GG了。

game(源于yali)

Description
小A和小B在玩一个游戏,他们两个人每人有n张牌,每张牌有一个点数,并且在接下来的n个回合中每回合他们两人会分别打出手中的一张牌,点数严格更高的一方得一分,然而现在小A通过某种神秘的方法得到了小B的出牌顺序,现在他希望规划自己的出牌顺序使得自己在得分尽可能高的前提下出牌的字典序尽可能大。

Input Format
第一行一个正整数n表示游戏进行的轮数。接下来一行n个整数,第i个数表示第i轮小B将要打出的牌的点数。接下来一行n个整数,表示小A拥有的牌的点数。

Output Format
输出一行n个整数,表示小A出牌的顺序。

Sample Input

5
1 2 3 4 5
3 2 2 1 4

Sample Output

2 3 4 2 1

Constraints
对于20%的数据,n ≤ 10对于40%的数据,n ≤ 3000对于60%的数据,n ≤ 6000对于100%的数据,n, a i a_i ai≤ 100000

简要思路:这题最恶心的恐怕就是字典序了,第一次做根本毫无头绪,只好爆零。
后面看了题解才发现这题竟然用到了线段树!?!
这题我们想知道小A的最高得分并不难,随便输出一种方案也不难,难就难在我们只能输出一个字典序最大的方案。试想一下,如果我们能够拥有这样一个神奇的东西(?)能够快速判断在某一位填某个数是否可行并且支持高效修改那该多好。经过一番思考,我想到了线段树。
这棵线段树的定义并不复杂,只有三个元素要维护,分别是 l l l r r r s s s。其中:
s s s表示当前区间小A可以得到的最高分;
l l l表示小A未成功对抗小B从而得分的小A的卡牌数;
r r r表示小B未被对抗的卡牌数。
因为卡牌的值不大(实在不行还有离散化呢),我们可用线段树的下标来代表卡片的值,这样进行区间合并就方便了。代码如下所示:

inline void update( int root ) {
	int delta = min( l[rs] , r[ls] );
	s[root] = s[ls] + s[rs] + delta;
	l[root] = l[ls] + l[rs] - delta;
	r[root] = r[ls] + r[rs] - delta;
	return;
}

这里的delta是用来统计 l l l, r r r两者中的较小值,位于左儿子区间的小B的卡牌一定会被右儿子区间的小A的卡牌成功对抗,至于小A能得多少分取决于两者中数目少的一方,另一方保留一点实力来服务于后面的统计。怎么有一种CDQ分治的味道
然后就二分来填数,先考虑这一局得分,如果不行再考虑不得分,填数尽量要填大一点的,通过线段树统计对总得分的影响来判断是否可行,然后就结束了。

#include <iostream>
#include <cstring>
#include <cstdio>
#include <set>
#include <algorithm>
#define N 100005
using namespace std;
int n , tot;
multiset<int> a;
int b[N];
inline int min( int a , int b ) {
	return a < b ? a : b;
}
struct semgentTree{
	#define ls root << 1
	#define rs root << 1 | 1
	int l[N << 2];
	int r[N << 2];
	int s[N << 2];
	inline void update( int root ) {
		int delta = min( l[rs] , r[ls] );
		s[root] = s[ls] + s[rs] + delta;
		l[root] = l[ls] + l[rs] - delta;
		r[root] = r[ls] + r[rs] - delta;
		return;
	}
	inline void insert( int root , int le , int ri , int pos , int vala , int valb ) {
		if ( le == ri ) {
			l[root] += vala;
			r[root] += valb;
			return;
		}
		int mid = ( le + ri ) >> 1;
		if ( pos <= mid ) {
			insert( ls , le , mid , pos , vala , valb );
		} else {
			insert( rs , mid + 1 , ri , pos , vala , valb );
		}
		update(root);
		return;
	}
}tree;//封装线段树方便搬运(手动滑稽~)
template < typename T >
inline void read( T & res ) {
	T pd = 1;
	res = 0;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
}
inline void getans( int p ) {
	tree.insert( 1 , 1 , n , b[p] , 0 , -1 );
	int le = b[p] + 1;
	int ri = *a.rbegin();
	while ( le < ri ) {
		int mid = ( le + ri + 1 ) >> 1;
		/*二分时可能得到一个原本小A就没有的卡牌数值,参见线段树的
		区间合并过程,处理不存在的数值会产生负数,减小tree.s[1]的值
		不会影响答案*/
		tree.insert( 1 , 1 , n , mid , -1 , 0 );
		if ( tree.s[1] + 1 == tot ) {
			le = mid;
		} else {
			ri = mid - 1;
		}
		tree.insert( 1 , 1 , n , mid , 1 , 0 );
	}
	//优先考虑得分,如果不行再考虑不得分
	tree.insert( 1 , 1 , n , le , -1 , 0 );
	if ( le <= ri && tree.s[1] + 1 == tot ) {
		tot--;
		printf("%d ",le);
		a.erase(a.find(le));
		return;
	}
	tree.insert( 1 , 1 , n , le , 1 , 0 );
	le = 1;
	ri = b[p];
	while ( le < ri ) {
		int mid = ( le + ri + 1 ) >> 1;
		tree.insert( 1 , 1 , n , mid , -1 , 0 );
		if ( tree.s[1] == tot ) {
			le = mid;
		} else {
			ri = mid - 1;
		}
		tree.insert( 1 , 1 , n , mid , 1 , 0 );
	}
	tree.insert( 1 , 1 , n , le , -1 , 0 );
	printf("%d ",le);
	a.erase(a.find(le));
	return;
}
int main () {
//	freopen( "game.in" , "r" , stdin );
//	freopen( "game.out" , "w" , stdout );
	read(n);
	for ( int i = 1 ; i <= n ; ++i ) {
		read(b[i]);
		tree.insert( 1 , 1 , n , b[i] , 0 , 1  ); 
	}
	int tem;
	for ( int i = 1 ; i <= n ; ++i ) {
		read(tem);
		tree.insert( 1 , 1 , n , tem , 1 , 0 );
		a.insert(tem);
	}
	tot = tree.s[1];
	for ( int i = 1 ; i <= n ; ++i ) {
		getans(i);
	}
	return 0;
}

从这可以看出,可以利用对二分区间的不同处理策略来构造数据结构来达到出人意料的结果,实现一些判断是否可行的优(luan)秀(gao)结果。

走夜路(源于本校OJ)

题目描述
Jim是一个胆小的男生,可是每天他都要学习到很晚才能回家,而且他回家的路上没有路灯。Jim非
常怕黑,万幸的是他还有一个手电筒可以用,我们设手电筒的电量上限为T。在Jim回家的路上有
(N+1)个充电站,0是起点N是终点,Jim每走一个单位距离消耗1个单位的电量.给出每个充电站到
下一个充电站的距离D,以及冲单位电量的花费P,求整个旅途的最少花费。如果Jim无法保证全程
手电筒都亮着输出-1.
对于30%的数据 N<=50
对于100%的数据 N<=500000

输入
第1行: 2个数N, T中间用空格分隔,N + 1为充电站的数量,T为手电筒的电池容量(2≤N≤500000,
1≤T≤10^9)。
第2至N + 1行:每行2个数D[i], P[i],中间用空格分隔,分别表示到下一个充电站的距离和充电的
单价(1≤D[i], P[i]≤1000000)。

输出
输出走完整个旅程的最小花费,如果无法保证手电筒全程照亮,输出-1

样例输入

3 15
10 2
9 1
8 3

样例输出

41

提示
样例解释
D = {10, 9, 8}, P = {2, 1, 3},T = 15,最小花费为41:在0冲10个单位的电,在1冲15个单位的电,
在2冲2个单位的电,刚好到家用完所有的电。
对于30%的数据 N<=50
对于100%的数据 N<=500000

简要思路:这题有一个显然的贪心,那就是如果在手电筒电量范围内能走到的最近的比当前电站电费少的电站,那么在当前电站充刚好可以到达那个电站的电量,到了那个电站再另作决定;否则,就在当前电站充满电,因为后面T范围内的电站都更昂贵,尽量少在那里充电(如果电费一样也不会导错,因为这是一个一个电站处理的,在某些电站只判断不充电)。
这题的难点在于如何快速预处理从当前站能到达的电费比当前站低的电站。可用单调栈从后往前处理,具体实现见代码吧。

#include <iostream>
#include <cstdio>
#include <cstring>
#define ll long long
#define N 500005 
using namespace std;
int d[N] , p[N] , q[N] , nxt[N];
ll len , now , cost , dis[N];
int n , t;
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline ll min( ll a , ll b ) {
	return a < b ? a : b;
}
int main () {
	//freopen( "wayhome.in" , "r" , stdin );
	//freopen( "wayhome.out" , "w" , stdout );
	read(n);
	read(t);
	len = cost = now = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		read(d[i]);
		read(p[i]);
		dis[i] = dis[i - 1] + 1ll * d[i];
	}
	//以下为单调栈
	int top = 0;
	for ( int i = n ; i >= 1 ; --i ) {
		while ( top && p[q[top]] > p[i] ) {
			top--;
		}
		nxt[i] = q[top];
		q[++top] = i;
		if ( !nxt[i] ) {
			nxt[i] = n + 1;
		}
	}
	//以上为单调栈
	for ( int i = 1 ; i <= n ; ++i ) {
		now -= d[i - 1];
		if ( now < 0 ) {
			puts("-1");
			return 0;
		}
		len = min( dis[nxt[i] - 1] - dis[i - 1] , t );
		if ( len > now ) {
			cost = cost + 1ll * ( len - now ) * p[i];
			now = len;
		}
	}
	printf("%lld",cost);
	return 0;
}

由此可见,单调栈可实现一些我们人类正常思维理解不了的东西,有时候思路匮乏时可试一试单调栈。
下一题也是单调栈,用来加深印象

灵知的太阳信仰

Description
在炽热的核熔炉中,居住着一位少女,名为灵乌路空。
据说,从来没有人敢踏入过那个熔炉,因为人们畏缩于空所持有的力量——核能。
核焰,可融真金。
咳咳。
每次核融的时候,空都会选取一些原子,排成一列。然后,她会将原子序列分成一些段,并将每段进行一次核融。
一个原子有两个属性:质子数和中子数。
每一段需要满足以下条件:
1、同种元素会发生相互排斥,因此,同一段中不能存在两个质子数相同的原子。
2、核融时,空需要对一段原子加以防护,防护罩的数值等于这段中最大的中子数。换句话说,如果这段原子的中子数最大为x,那么空需要付出x的代价建立防护罩。求核融整个原子序列的最小代价和。

Input
第一行一个正整数N,表示原子的个数。
接下来N行,每行两个正整数pi和ni,表示第i个原子的质子数和中子数。

Output
输出一行一个整数,表示最小代价和。

Sample Input

5
3 11
2 13
1 12
2 9
3 13

Sample Output

26

Data Constraint
对于20%的数据,1<=n<=100
对于40%的数据,1<=n<=1000
对于100%的数据,1<=n<=10 ^ 5,1<=pi<=n,1<=ni<=2*10 ^ 4

简要思路:本来呢,这题暴力是只能拿四十分的,但是,有一位大佬证明,有技巧的暴力是能过的(每次得到一个位置i向左,走到相同数为止),这有一个栗子

(大佬的证明)假设 i 步就结算,那么花费了 i 步,概率是 i-1 步内未结束,在第 i 步时结束了。
P(i-1 步内未结束)=P(前 i-1 个互不相同)=(p/p)[(p-1)/p][(p-2)/p]……[(p-i)/p]=p!/[(p-i-1)!*p^(i-1)]
P(在第 i 步恰好结束)=P(i-1 步内未结束)*P(第 i 个与前 i-1 个中某一个相同)=p!/[(p-i-1)!p^(i-1)](i-1)/p
对期望的贡献为 P(在第 i 步恰好结束)*i
我们计算一下 10^5 时的期望,大约为 400+。
因此该暴力算法的期望复杂度确实能通过全部的数据。

不过,我们为了追求不被有心的出题人卡,同时复习一下单调栈,应该想一个更优的方法(单调栈要登场了)。
根据题意,不难推出方程 d p [ i ] = d p [ j ] + m a x dp[i] = dp[j] + max dp[i]=dp[j]+max{ n [ j + 1 ] n[j + 1] n[j+1] ~ n [ i ] n[i] n[i] };用数组 m a x n maxn maxn记录加号后面式子的值。
同时, l l l数组记录每个数能向左拓展的最大范围,用单调队列维护数据, h e a d head head的位置在当前值最大范围以外,踢出;对于 t a i l tail tail,踢出中子数不如当前中子数的,维护一个单调递增的单调栈
处理完后,当前值就可以入单调队列了。
不过此时有两种情况:
如果队列中只有一个元素(刚加进来的),直接更新;
如果不止一个,由于最靠前的在当前值的活动范围之外,要特地更新一下(先出来再进去)。
还有疑问的看代码吧。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <set>
using namespace std;
const int N = 1e5 + 5;
int numa[N] , numb[N] , l[N] , now[N] , sum[N] , maxn[N] , pos[N] , dp[N];
multiset <int> q;//它的作用是自动排序,并删除指定值,multiset可存值相同的元素 
int n , head , tail;
inline void read( int & res ) {
	res = 0;
	int pd = 1;
	char a = getchar();
	while ( a < '0' || a > '9' ) {
		if ( a == '-' ) {
			pd = -pd;
		}
		a = getchar();
	}
	while ( a >= '0' && a <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( a - '0' );
		a = getchar();
	}
	res *= pd;
	return;
}
int main () {
	//freopen( "array.in" , "r" , stdin );
	//freopen( "array.out" , "w" , stdout );
	read(n);
	for ( int i = 1 ; i <= n ; ++i ) {
		read(numa[i]);
		read(numb[i]);
		l[i] = now[numa[i]] + 1;//统计当前数的活动范围 
		now[numa[i]] = i;
		l[i] = max( l[i] , l[i - 1] );//左边数的范围也会对当前数的范围造成限制 
	}
	head = 1;
	tail = 0;
	for ( int i = 1 ; i <= n ; ++i ) {
		int k = i - 1;
		while ( head < tail && l[i] > pos[head + 1] ) {//踢出活动范围以外的 
			q.erase(q.find(sum[head]));//其实留了一个,对应后面改队头的操作
			head++;
		}
		while ( head <= tail && numb[i] > maxn[tail] ) {//维护单调栈 
			q.erase(q.find(sum[tail]));
			k = pos[tail]; 
			tail--;
		}
		pos[++tail] = k;
		maxn[tail] = numb[i];
		if ( head != tail ) {
			sum[tail] = dp[pos[tail]] + maxn[tail];
			q.insert(sum[tail]);
			q.erase(q.find(sum[head]));
		}
		sum[head] = dp[l[i] - 1] + maxn[head];
		q.insert(sum[head]);
		dp[i] = *q.begin();
	}
	printf("%d",dp[n]);
	return 0;
}

宝石专家(源于本校OJ)

题目描述
Jim是一位宝石收藏品行家,在他的收藏室里保存着许多珍贵的宝石,磷叶石、钻石、摩根石、透
绿柱石….,已知Jim有n个宝石,现在他将这n个宝石从1到n排开编号从1到n。Jim发现他所有的宝
石中竟然有不少是完全相同的的,我们规定每个宝石都有一个特征值ai,当两个宝石特征值相等时
及认为两个宝石相同。Jim发现两个相同的宝石离得越接近越明显。Jim现在有m个问题,他想问你
在.编号l到r这一区间里的所有宝石中,两个相同宝石的最近距离是多少,(两个宝石的距离是它们
编号的绝对值之差)。
保证 l < r ,对于 ax和ay 若ax=ay 它们的距离为|x-y|。

输入
单组测试数据。
第一行有两个整数n, m (1≤n,m≤2*10^5),表示宝石序列的长度和查询的次数。
第二行有n个整数a1,a2,…,an (-10^9 ≤ai≤10^9),ai表示第i个宝石的特征值。
接下来有m行,每一行给出两个整数lj,rj (1≤lj≤rj≤n)表示一个查询。
对于 10%的数据 保证 n<=20
对于 50%的数据 保证 n<=50000
对于 100%的数据 保证 n<=200000

输出
对于每一个查询,输出最近的距离,如果没有相等的元素,输出-1。

样例输入

5 3
1 1 2 3 2
1 5
2 4
3 5

样例输出

1
-1
2

提示
样例解释
第一个询问 第一个和第二个宝石最近且相同 距离为 1
第二个询问 第二个宝石到第四个宝石之间没有相同宝石 输出 -1
第三个询问 第三个宝石和第五个宝石最近且相同 距离为2
对于 10%的数据 保证 n<=20
对于 50%的数据 保证 n<=50000
对于 100%的数据 保证 n<=200000

简要思路:这题不用想复杂了。我们先将输入的宝石的值离散化,然后将每对同宝石中相邻的两个宝石用结构体保存,按左边坐标为第一关键字,右边坐标为第二关键字排序。然后对询问用同样的方法排序。
倒序处理询问,当当前询问左坐标小于当前点对的左坐标时,将当前点按“在位置为当前点对的右坐标的地方插入值(R-L)”插入线段树。处理询问时访问区间[0,R]即可。本题主要是看代码更好理解。
这题最难得可贵的思想是降维思想,一个点对能对询问 [ l , r ] [l,r] [l,r]产生贡献,当且仅当这对点左边的在 l l l上或右边,右边的点在 r r r上或左边。通过排序单调递减处理询问的 l l l值,同时只加入两个点都在 l l l以及它的右端的点对,这样只要一维考虑 r r r及其左端的贡献即可,没有后顾之忧,直接用最简单的线段树(树状数组)处理即可。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N 200005
#define ls root << 1
#define rs root << 1 | 1
using namespace std;
int n , m , mcnt , ncnt;
int num[N] , nxt[N] , last[N] , ans[N];
struct Query{
	int l , r;
	int id;
}q[N];
struct Node{
	int l , r;
}node[N];
struct A{
	int x , id;
}a[N];
struct ST{
	int minn;
	int l , r;
}tree[N << 2];
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline bool cmpq( Query a , Query b ) {
	return a.l < b.l;
}
inline bool cmpn( Node a , Node b ) {
	return a.l < b.l;
}
inline bool cmpa( A a , A b ) {
	return a.x < b.x;
}
inline int min( int a , int b ) {
	return a < b ? a : b; 
}
inline void build( int root , int l , int r ) {
	tree[root].l = l;
	tree[root].r = r;
	tree[root].minn = 0x3f3f3f3f;
	if ( l == r ) {
		return;
	}
	int mid = ( l + r ) >> 1;
	build( ls , l  , mid );
	build( rs , mid + 1 , r );
	return;
}
inline void modify( int root , int pos , int val ) {
	if ( tree[root].l == tree[root].r ) {
		tree[root].minn = val;
		return;
	}
	int mid = ( tree[root].l + tree[root].r ) >> 1;
	if ( pos <= mid ) {
		modify( ls , pos , val );
	} else {
		modify( rs , pos , val );
	}
	tree[root].minn = min( tree[ls].minn , tree[rs].minn );
	return;
}
inline int query( int root , int l , int r ) {
	if ( l <= tree[root].l && tree[root].r <= r ) {
		return tree[root].minn;
	}
	int ans = 0x3f3f3f3f;
	int mid = ( tree[root].l + tree[root].r ) >> 1;
	if ( l <= mid ) {
		ans = min( ans , query( ls , l , r ) );
	}
	if ( r >= mid + 1 ) {
		ans = min( ans , query( rs , l , r ) );
	}
	return ans;
}
int main () {
	freopen( "jewel.in" , "r" , stdin );
	freopen( "jewel.out" , "w" , stdout );
	read(n);
	read(m);
	mcnt = ncnt = 0;
	//↓是离散化
	for ( int i = 1 ; i <= n ; ++i ) {
		read(a[i].x);
		a[i].id = i;
	}
	sort( a + 1 , a + 1 + n , cmpa );
	num[a[1].id] = ++mcnt;
	for ( int i = 2 ; i <= n ; ++i ) {
		if ( a[i].x != a[i - 1].x ) {
			num[a[i].id] = ++mcnt;
		} else {
			num[a[i].id] = mcnt;
		}
	}
	//↑是离散化
	for ( int i = 1 ; i <= n ; ++i ) {
		if ( !last[num[i]] ) {
			last[num[i]] = i;
		} else {
			node[++mcnt].l = last[num[i]];
			node[mcnt].r = i;
			last[num[i]] = i;
		}
	}
	for ( int i = 1 ; i <= m ; ++i ) {
		read(q[i].l);
		read(q[i].r);
		q[i].id = i;
	}
	build( 1 , 1 , n );
	sort( node + 1 , node + 1 + mcnt , cmpn );
	sort( q + 1 , q + 1 + m , cmpq );
	int pn = mcnt;
	for ( int i = m ; i >= 1 ; --i ) {
		while ( pn >= 1 && node[pn].l >= q[i].l ) {
			modify( 1 , node[pn].r , node[pn].r - node[pn].l );
			pn--;
		}
		ans[q[i].id] = query( 1 , 1 , q[i].r );
		if ( ans[q[i].id] == 0x3f3f3f3f ) {
			ans[q[i].id] = -1;
		}
	}
	for ( int i = 1 ; i <= m ; ++i ) {
		printf("%d\n",ans[i]);
	}
	return 0;
}

通过这题可以发现,进行非莫队算法用数据结构巧妙地处理询问不失为解题的一种好方法。不会莫队的蒟蒻的福音。

新婚快乐(源于本校OJ)

题目描述
Jim马上就要结婚了,尽管Jim自己本身不是很有钱,但他还是一个很讲究”排面”的人。没有什么能
比得上长长的婚车队列更吸引眼球。为了宴请八方,Jim包了 辆车来接送亲友。不过,由于Jim
的亲戚朋友到达时间不同,每辆车的出发时间也不同。为了更好的迎接宾客,Jim希望能知道每辆
车到达目的地的时间。
我们可以将起点到目的地的路线看作一条经过 个十字路口的直线,在每一个路口都有一个红绿
灯,并且在红灯和绿灯之间周期性切换。一开始,红绿灯显示为绿灯。绿灯将会持续 秒钟,此
时,车允许通行。之后转换为红灯,红灯持续 秒钟。当红绿灯显示为红灯时,只有以下一种情
况要停车,既如果车刚好到达十字路口时,红绿灯显示为红灯,则要停车等待,等转换为绿灯后
q
n
g
r继续通行。但是,如果车刚好到达十字路口时,红绿灯刚好转换为绿灯,则无需等待。另外,车
在两个十字路口之间的路上行驶时,无论是否红灯,都可以通行。
在此基础上,所有的红绿灯都是同步的,及所有的红绿灯转换周期都一样。所有红绿灯开始都为
绿色。
在整个路线上,有 n + 1 个路段。一个路段指的是两个连续的红绿灯或者红绿灯与起点(或终
点)之间的一段路。从起点到第一个红绿灯为一个路段,每两个连续的红绿灯之间为一个路段,
最后一个红绿灯与终点为一个路段。每一辆车通过每一路段的时间为 li 。Jim提供 n + 1 个正整
数 li ( 1 ≤ i ≤ n + 1,1 ≤ li ≤ 10 ^ 9 ),代表车通过从起点到终点的第 i 个路段的时间为 li
秒钟。 l1 就表示车通过从起点到第一个红绿灯的时间。 ln+1 则表示车通过最后一个红绿灯到
终点的时间。
q 辆车每辆车的出发时间为 ti 秒。计算每辆车到达终点的时间,(车辆视作质点,且不会互相阻
碍)

输入
单组测试数据。
第一行,有三个以空格隔开的正整数n,g,r(1≤n≤2105,2≤g+r≤2*109)分别表示十字路
口的个数,绿灯持续的秒数,红灯持续的秒数。
接下来一行有n+1个整数li(1≤i≤n+1,1≤li≤10^9)表示通过第i个路段所需要的时间。
接下来一行有一个整数q(1≤q≤2
10^5)车的数量。
接下来有q行,每一行有一个整数ti(1≤i≤q,1≤ti≤10^9)表示第i辆车从起点出发的时间。

数据范围
对于50%的数据 保证 n <= 5000
对于100%的数据 保证 n <= 2e5

输出
有q行,每行一个整数,第i行表示第i辆车到达终点的最快时刻。

样例输入

4 5 1 
12 18 4 6 7 
5
4
11
5
3
2

样例输出

51
59
53
50
49

提示
样例解释
第2辆车会在第一个红绿灯停下 第3辆车也会在第一个红绿灯停下 其余车辆皆顺利通过

简要思路:抄自老师的PDF
首先我们先考虑整个问题,对于每一辆车在整个行驶的过程中都可能会碰到红灯,在碰到红灯的
等待后再通过其它红绿灯的时间都可以预处理了,是一个给定的值。我们不妨先将问题简化,假设车在第i个红绿灯路
口出发到终点的时间是多少。对于简化后的问题可以应用动态规划的思路,设dp[i]为车从第i个红
绿灯路口刚好绿灯开始出发到终点的时间,设x为车启动后遇到的第二个红灯(x>i),则 d p [ i ] = d p [ x ] + d i s ( i , x ) + w a i t ( x ) dp[i] = dp[x] + dis(i,x) + wait(x) dp[i]=dp[x]+dis(i,x)+wait(x)。dis 表示两个红绿灯之间的距离,wait表示在红灯处的等待时间。现在
我们遇到的问题主要是如何确定当前点出发后遇到的下一个红灯。
为解决这个问题,我们将所有点到起点的距离模上(re+ge) 一个红绿灯周期 (以下皆用 mod代替)
,是其中的点落在[ 0,mod-1]这一区间。
由于数据范围的原因,整个过程需要离散化,其中距离取模排序后的第 1 2 3 4 在绿灯区 5 6 7 8在
红灯区。也就意味着如果从头出发,车辆可以顺利通过第 1 2 3 4 个红绿灯。在红灯区我们们会被
编号最小的红绿灯挡住,接下来的任务就是找到红灯区编号最小的红绿灯。
根据dp的顺序,我们可以将整个序列倒序枚举并用一个线段树维护红灯区编号最小值。
但是,我们又面临了一个新的问题,我们在线段树里存的是每个点到起点的距离,而不是两个点
的相对距离。假设对于点i,此时线段树中存的是所有x > i 的点距离起点的值而不是和i的相对距
离。为了解决这一问题,我们可以避免区间加减这一问题,设线段树内当前值为Ax 相对距离为
Ax-dis(i,0)。设红灯区的左右端点分别为L,R。
若 L < Ax-dis(i,0) < R 及点x在红灯区,我们可以采取一个小变形,即将访问区间改为 L + d i s ( i , 0 ) L+ dis(i,0) L+dis(i,0)
R + d i s ( i , 0 ) R+ dis(i,0) R+dis(i,0) 即可。
由于模运算的性质,红绿灯区可能会变成两种情况。
1.红灯区在区间中间 2.绿灯区在区间中间
可以分类讨论。
接下来我们考虑查询,对于延误的时间t,我们可以采用上文的更换区间的方式解决,之后我们利
用线段树查询出我们最先被卡住的红灯x,答案即为 d i s ( 0 , x ) + w a i t ( x ) + d p [ x ] dis(0,x)+wait(x)+dp[x] dis(0,x)+wait(x)+dp[x], wait(x)为等待时间。
综合考虑我们只使用了一个线段树维护信息,综合上离散化总复杂度 O(nlogn)。

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define ll long long
#define N 200005
#define ls root << 1
#define rs root << 1 | 1
using namespace std;
int n;
ll ge , re , last;
int t[N << 2];
ll dp[N] , a[N] , map[N] , h[N];
struct Node{
	int rk , id;
	ll val;
}node[N];
template < typename T >
inline void read( T & res ) {
	res = 0;
	T pd = 1;
	char aa = getchar();
	while ( aa < '0' || aa > '9' ) {
		if ( aa == '-' ) {
			pd = -pd;
		}
		aa = getchar();
	}
	while ( aa >= '0' && aa <= '9' ) {
		res = ( res << 1 ) + ( res << 3 ) + ( aa - '0' );
		aa = getchar();
	}
	res *= pd;
	return;
}
inline bool cmpa( Node a , Node b ) {
	return a.val < b.val;
}
inline bool cmpb( Node a , Node b ) {
	return a.id < b.id;
}
inline void update( int root , int l , int r , int pos , int val ) {
	if ( l == r ) {
		t[root] = val;
		return;
	}
	int mid = ( l + r ) >> 1;
	if ( pos <= mid ) {
		update( ls , l , mid , pos , val );
	} else {
		update( rs , mid + 1 , r , pos , val );
	}
	t[root] = min( t[ls] , t[rs] );
	return;
}
inline int query( int root , int l , int r , int x , int y ) {
	if ( x <= l && r <= y ) {
		return t[root];
	}
	int mid = ( l + r ) >> 1;
	int ans = 1e9 + 500;
	if ( mid >= x ) {
		ans = min( ans , query( ls , l , mid , x , y ) );
	}
	if ( y >= mid + 1 ) {
		ans = min( ans , query( rs , mid + 1 , r , x , y ) );
	}
	return ans;
}
inline int getpos( ll l , ll r ) {
	ll posl = lower_bound( h + 1 , h + 1 + n , l ) - h;
	ll posr = lower_bound( h + 1 , h + 1 + n , r ) - h;
	ll posrr = upper_bound( h + 1 , h + 1 + n , r ) - h;
	if ( posr == posrr ) {
		posr--;
	} else {
		posr = posrr - 1;
	}
	if ( posr > n ) {
		posr = 1;
	}
	if ( posl < 1 ) {
		posl = 1;
	}
	if ( posl > posr ) {
		return 1e9 + 500;
	} else {
		return query( 1 , 1 , n , posl , posr );
	}
}
int main () {
	read(n);
	memset( t , 0x3f , sizeof(t) );
	read(ge);
	read(re);
	ll mod = ge + re;
	for ( int i = 1 ; i <= n ; ++i ) {
		read(a[i]);
		map[i] = map[i - 1] + a[i];
		node[i].val = map[i] % mod;
		node[i].id = i;
		h[i] = node[i].val;
	}
	read(last);
	sort( h + 1 , h + 1 + n );
	sort( node + 1 , node + 1 + n , cmpa );
	for ( int i = 1 ; i <= n ; ++i ) {
		node[i].rk = i;
	}
	sort( node + 1 , node + 1 + n , cmpb );
	for ( int i = n ; i >= 1 ; --i ) {
		ll tem = map[i];
		tem %= mod;
		int id = 1e9 + 500;
		ll l = ( ge + tem + mod ) % mod;
		ll r = ( ge + tem + re - 1 + mod ) % mod;
		if ( r < l ) {
			int id1 = getpos( l , mod );
			int id2 = getpos( 0 , r );
			id = min( id1 , id2 );
		} else {
			id = getpos( l , r );
		}
		if ( id < 1e9 ) {
			ll cur = map[id] - map[i];
			dp[i] = dp[id] + cur + ( mod - cur % mod );
		} else {
			dp[i] = map[n] - map[i];
		}
		update( 1 , 1 , n , node[i].rk , i );
	}
	int q;
	read(q);
	while ( q-- ) {
		ll t;
		read(t);
		ll ans;
		ll tem = t % mod;
		ll l = ( ge - tem + mod ) % mod;
		ll r = ( ge - tem + mod + re - 1 ) % mod;
		int id = 1e9 + 500;
		if ( r < l ) {
			int id1 = getpos( l , mod );
			int id2 = getpos( 0  , r );
			id = min( id1 , id2 );
		} else {
			id = getpos( l , r );
		}
		if ( id > 1e9 ) {
			ans = t + last + map[n];
		} else {
			ll cur = t + map[id];
			ans = cur + last + dp[id] + ( mod - cur % mod ) % mod;
		}
		printf("%lld\n",ans);
	}
	return 0;
}

这是用线段树解决区间覆盖问题,不见真的不知道啊。

就总结到这吧,以后可能会加。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值