洛谷2019秋令营 网课第一节

本文深入探讨了数据预处理技术,如离散化和前缀和的运用,并详细讲解了差分和前缀和算法在解决特定问题上的应用,包括一维和二维场景。此外,文章还介绍了树上差分技巧以及贪心算法的不同变种,提供了丰富的洛谷平台实例代码,帮助读者理解算法原理和实践操作。

首先讲了数据的预处理,包括离散化和前缀和的东西。

前缀和:

洛谷P3406

关键差分公式为了使区间[l,r]区间加上1 我们需要:

                                                                       \\sum[l]+=1\\ sum[r+1]-=1

前缀和公式:

                                                                      sum[l]+=sum[l-1]+arr[l]

其中arr表示数组,若我们只想用差分数组出来前缀和,这个arr可以不用。

为了让[x1,y1]到[x2,y2]的区间加1

二维差分公式:

                                                                        \\sum[x1][y1]+=1\\ sum[x2+1][y2+1]+=1\\ sum[x1][y2+1]-=1\\ sum[x2+1][y1]-=1\\

二维前缀和:

                                                                       sum[x][y]+=sum[x-1][y]+sum[x][y-1]-sum[x-1][y-1]

二维差分需要二位前缀和来恢复。

洛谷P3406

#include <bits/stdc++.h>
#define int long long
using namespace std;
int32_t main(){
	int n,m;cin>>n>>m;
	vector<int> mv;
	for(int i=0;i<m;i++){
		int t;cin>>t;
		mv.push_back(t);
	}
	vector<vector<int> > arrmv(n,vector<int>(3,0));
	for(int i=0;i<n-1;i++){
		cin>>arrmv[i][0]>>arrmv[i][1]>>arrmv[i][2];
	}
	int dif[n+1];
	memset(dif,0,sizeof(dif));
	for(int i=0;i<m-1;i++){
		int a=mv[i];
		int b=mv[i+1];
		if(b<a)swap(a,b);
		dif[a]+=1;
		dif[b]-=1;
	}
	for(int i=1;i<n;i++){
	dif[i]+=dif[i-1];}
	// for(int i=1;i<=n-1;i++)cerr<<dif[i]<<endl;

	int ans=0;
	for(int i=1;i<=n-1;i++){
		if(dif[i]*arrmv[i-1][0]>(dif[i]*arrmv[i-1][1]+arrmv[i-1][2])){
		ans+=dif[i]*arrmv[i-1][1]+arrmv[i-1][2];}
		else ans+=dif[i]*arrmv[i-1][0];
	}
		cout<<ans<<endl;
	
	return 0;
}

洛谷1115

#include <bits/stdc++.h>
using namespace std;
const int inf=1e9;
int main(){
	int n;cin>>n;
	vector<int> arrmv(n,0);
	for(int i=0;i<n;i++)
		cin>>arrmv[i];
	vector<int> sumarr(n+1,0);
	for(int i=0;i<n;i++){
		sumarr[i+1]+=sumarr[i]+arrmv[i];
	}
	int t=sumarr[0];
	int ans=-inf;
	for(int i=1;i<=n;i++){
		
		ans=max(ans,sumarr[i]-t);
		t=min(sumarr[i],t);
	}
	cout<<ans<<endl;
	return 0;
}

洛谷3397

#include <bits/stdc++.h>
using namespace std;
const int MAXN=1e3+10;
int arr[MAXN][MAXN];
int main(){
	memset(arr,0,sizeof(arr));
	int n,m;cin>>n>>m;
	for(int i=0;i<m;i++){
		int x1,y1,x2,y2;
		cin>>x1>>y1>>x2>>y2;
		x1-=1;y1-=1;x2-=1;y2-=1;
		arr[x1][y1]+=1;
		arr[x2+1][y2+1]+=1;
		arr[x2+1][y1]-=1;
		arr[x1][y2+1]-=1;
	}
	for(int i=0;i<n;i++)
		for(int j=0;j<n;j++){
			if(i)arr[i][j]+=arr[i-1][j];
			if(j)arr[i][j]+=arr[i][j-1];
			if(i &&j)arr[i][j]-=arr[i-1][j-1];
		}
	for(int i=0;i<n;i++){
		for(int j=0;j<n;j++){
			if(j)cout<<" ";
			
			cout<<arr[i][j];
		}
	cout<<endl;
	}
	
	return 0;
}

树上对点或者边差分

当我们需要频繁地对树上任意路径的边权值或者点权值加w时候,可以使用这一个方法。复杂度为o(nlogn).主要复杂度在计算lca上面。

首先我们写出树上对点的差分公式:

                                                         \\diff[u]+=w\\ diff[v]+=w\\ diff[lca(u,v)]-=w\\ diff[parent[lca(u,v)]]-=w

其中diff为差分数组,记录的是每个点的权值变化。后面需要使用树上前缀和恢复。lca为最近邻公共祖先,一般使用倍增的方法实现:

https://www.geeksforgeeks.org/lca-for-general-or-n-ary-trees-sparse-matrix-dp-approach-onlogn-ologn/

parent代表父亲数组,记录的是当前节点的父亲。在计算lca时候,在用倍增计算父亲时,我们可以计算当前节点的第2^i个父亲,所以这个parent数组应该是在计算lca时候得到的。

注意,u和v下标都从1开始,这样方便计算。主要是因为里面用了parent数组,假如从1做下标,我们就免去了处理0点的parent的特判。然后是树上对边差分,公式如下:

                                                              \\diff[u]+=w\\ diff[v]+=w\\ diff[lca(u,v)]-=2w

注意这里的diff数组指的是当前节点指向父亲节点的边。为了从差分恢复,我们需要计算树上前缀和。代码如下:

int dfs(int u){
	flag[u]=1;    //走过打个标记
	for(int i=0;i<(int)tree[u].size();i++){
		int nx=tree[u][i];
		if(flag[nx])continue;
		diff[u]+=dfs(nx);
	}
	return diff[u];
}

洛谷3258

这题是很好的对边差分的题目。这题看似是对点进行差分,但是我们发现假如使用对点进行差分的话,需要特判而且情况特别多。这里我们可以转换为对边差分。对于除了终止节点的其它节点有如下公式:

                                                                       (1+\sum_i cnt_i)/2

表示当前点的权值为相邻边走过的次数+1再除以2.对于终止节点,分子的+1需要去掉。

#include <bits/stdc++.h>
#define MAXN 300010 
#define level 19 
using namespace std;
vector <int> tree[MAXN]; 
int depth[MAXN]; 
int parent[MAXN][level]; 
// pre-compute the depth for each node and their 
// first parent(2^0th parent) 
// time complexity : O(n) 
void dfs(int cur, int prev) 
{ 
	depth[cur] = depth[prev] + 1; 
	parent[cur][0] = prev; 
	for (int i=0; i<tree[cur].size(); i++) 
	{ 
		if (tree[cur][i] != prev) 
			dfs(tree[cur][i], cur); 
	} 
} 

// Dynamic Programming Sparse Matrix Approach 
// populating 2^i parent for each node 
// Time complexity : O(nlogn) 
void precomputeSparseMatrix(int n) 
{ 
	for (int i=1; i<level; i++) 
	{ 
		for (int node = 1; node <= n; node++) 
		{ 
			if (parent[node][i-1] != -1) 
				parent[node][i] = 
					parent[parent[node][i-1]][i-1]; 
		} 
	} 
} 

// Returning the LCA of u and v 
// Time complexity : O(log n) 
int lca(int u, int v) 
{ 
	if (depth[v] < depth[u]) 
		swap(u, v); 

	int diff = depth[v] - depth[u]; 

	// Step 1 of the pseudocode 
	for (int i=0; i<level; i++) 
		if ((diff>>i)&1) 
			v = parent[v][i]; 

	// now depth[u] == depth[v] 
	if (u == v) 
		return u; 

	// Step 2 of the pseudocode 
	for (int i=level-1; i>=0; i--) 
		if (parent[u][i] != parent[v][i]) 
		{ 
			u = parent[u][i]; 
			v = parent[v][i]; 
		} 

	return parent[u][0]; 
} 

void addEdge(int u,int v) 
{ 
	tree[u].push_back(v); 
	tree[v].push_back(u); 
} 
vector<int> diff(MAXN,0);
vector<int> flag(MAXN,0);
int dfs2(int u){
	flag[u]=1;
	for(int i=0;i<(int)tree[u].size();i++){
		int nx=tree[u][i];
		if(flag[nx])continue;
		diff[u]+=dfs2(nx);
	}
	return diff[u];
}
		
		
		
int main(){
	memset(parent,-1,sizeof(parent));
	int n;cin>>n;
	
	vector<int> arrmv(n);
	for(int i=0;i<n;i++)cin>>arrmv[i];
	for(int i=0;i<n-1;i++){
		int a,b;cin>>a>>b;
		addEdge(a,b);
	}
	depth[0]=0;
	dfs(1,0); 
	
	precomputeSparseMatrix(n); 
	for(int i=0;i<n-1;i++){
		int a,b;
		a=arrmv[i];
		b=arrmv[i+1];
		int lcaa=lca(a,b);
		diff[a]+=1;
		diff[b]+=1;
		diff[lcaa]-=2;
			
	}
	dfs2(1);
	int ans[n+1];
	for(int i=1;i<=n;i++){
		ans[i]=diff[i];
	for(int ii=0;ii<(int)tree[i].size();ii++){
		int nx=tree[i][ii];
		if(nx==parent[i][0])continue;
		ans[i]+=diff[nx];
	}
	}
	for(int i=1;i<=n;i++){
		cout<<ans[i]<<endl;
	}
	
	return 0;
}

关于贪心:

序列贪心。序列贪心指的是,在一串序列中,我们需要通过不断安排元素的位置使得某一个量最大或者最小。做这种题假若从贪心入手,我们可以这样考虑。我们就假设有两个元素a和b分别将它们排列为a,b和b,a,然后比较量的关系。比如最大值之间的最小值。我们选出max(order(ab)),max(order(ba))做比较,得到最优的一个量后,我们得到一个关键字用作sort函数里面的cmp.

洛谷1842:

#include <bits/stdc++.h>
#define int long long
using namespace std;
bool cmp(pair<int,int> &fir ,pair<int,int> &las){
	if(fir.first+fir.second>las.first+las.second){
		return true;
	}else return false;
}
const int inf=1e18;
int32_t main(){
	int n;cin>>n;
	vector<pair<int,int>> arrmv(n);
	for(int i=0;i<n;i++){
		int a,b;cin>>a>>b;
		arrmv[i]=make_pair(a,b);
	}
	sort(arrmv.begin(),arrmv.end(),cmp);
	reverse(arrmv.begin(),arrmv.end());
	vector<int> presum(n+2,0);
	for(int i=1;i<=n;i++){
		presum[i]+=presum[i-1]+arrmv[i-1].first;
	}
	reverse(arrmv.begin(),arrmv.end());
	reverse(presum.begin(),presum.end());
	int ans=-inf;
	for(int i=0;i<n;i++){
		ans=max(ans,presum[i+2]-arrmv[i].second);
	}
	cout<<ans<<endl;
	return 0;
}

带反悔的贪心

所谓带反悔的贪心就是,我们一直选择贪心策略,但是,我们遇到一个有可能更优的策略时,我们去掉前面做的一个相对更劣的策略,加入这个新的策略,最后我们会全局最优。一般使用c++的优先队列来记录以前做过的策略,每次需要做新策率时候我们就比较即可。

洛谷P4053

#include <bits/stdc++.h>
#define int long long
using namespace std;
int32_t main(){
	int n;cin>>n;
	vector<pair<int,int>> mv;
	for(int i=0;i<n;i++){
		int a,b;cin>>a>>b;
		mv.push_back(make_pair(b,a));
	}
	sort(mv.begin(),mv.end(),less<pair<int,int>>());
	priority_queue<int,vector<int>,less<int>> pq;
	int lstime=0;
	for(int i=0;i<n;i++){
		if(lstime+mv[i].second<=mv[i].first){
			lstime=lstime+mv[i].second;
			pq.push(mv[i].second);
		}else{
			if(pq.size()){
				int no=pq.top();
				if(mv[i].second <no &&lstime-no+mv[i].second<=mv[i].first){
					pq.pop();
					pq.push(mv[i].second);
					lstime=lstime-no;
					lstime+=mv[i].second;
				}
			}
		}
	}
	cout<<pq.size()<<endl;
	return 0;
}

洛谷1230

#include <bits/stdc++.h>
using namespace std;
int main(){
    int m,n;cin>>m>>n;
    vector<pair<int,int>> arrmv(n);
    for(int i=0;i<n;i++){
        cin>>arrmv[i].first;
    }
    int sum=0;
    for(int i=0;i<n;i++){
        cin>>arrmv[i].second;
        sum+=arrmv[i].second;
    }
    sort(arrmv.begin(),arrmv.end(),less<pair<int,int>>());
    priority_queue<int,vector<int>,greater<int>> pq;
    int take=0;
    for(int i=0;i<n;i++){
        if(arrmv[i].first>take){
            take++;
            pq.push(arrmv[i].second);
        }else{
            int no=pq.top();if(arrmv[i].second>no){
                pq.pop();pq.push(arrmv[i].second);
            }
        }
    }
    while(!pq.empty()){
        int no=pq.top();
        pq.pop();
        sum-=no;
    }
    int ans=m-sum;
    cout<<ans<<endl;

        
    return 0;
}

 

关于这题我的暴力,为什么输出的答案不对? 扶苏疑惑(doubt) 【题目描述】 扶苏有一个序列 a。初始时,a 的长度为 1,仅含有 1 一个元素,将这个序列记作 a1。 对于 ai(i > 1),其构造方法是:将数列 ai−1 中所有数 i − 1 的左右两侧都插入一 个数 i。例如: a1 = [1] a2 = [2, 1, 2] a3 = [3, 2, 3, 1, 3, 2, 3] a4 = [4, 3, 4, 2, 4, 3, 4, 1, 4, 3, 4, 2, 4, 3, 4] · · · 现在,扶苏有 q 次询问,每次给定两个整数 n, k,你要回答:序列 an 的第 k 个数 是多少? 【输入格式】 从文件 doubt.in 中读入数据。 第一行是一个整数,表示询问数量 q。 接下来 q 行,每行两个整数表示 n, k。 【输出格式】 输出到文件 doubt.out 中。 对每个询问,输出一行一个整数表示序列 an 的第 k 个元素的值。 【样例 1 输入】 1 3 2 1 1 3 2 3 4 3 3 【样例 1 输出】 第 4 页 共 10 页 2025 洛谷秋令基础组 第一场 扶苏疑惑(doubt) 1 1 2 2 3 3 【样例 2】 见选手目录下的 doubt/doubt2.in 与 doubt/doubt2.ans。 【子任务】 • 对 100% 的数据,n ≤ 10。 时间限制:100s #include<bits/stdc++.h> using namespace std; const int MAXN = 1e5; int Q, N; struct qq{ int n, k; }q[MAXN]; vector<int> a[MAXN]; int main(){ cin >> Q; for( int i = 1; i <= Q; i ++ ){ cin >> q[i].n >> q[i].k; N = max( N, q[i].n ); } a[1].push_back( 1 ); for( int i = 2; i <= N; i ++ ){ for( int j = 1; j <= a[i - 1].size(); j ++ ){ if( a[i - 1][j] == i - 1 ){ a[i].push_back( i ); a[i].push_back( i - 1 ); a[i].push_back( i ); } else a[i].push_back( a[i - 1][j] ); } } for( int i = 1; i <= Q; i ++ ){ cout << a[q[i].n][q[i].k] << endl; } return 0; }
最新发布
10-05
给你完整题面,将你的代码修正正确 扶苏跑路(travel) 【题目描述】 某国共有 n 个城市,有 m 条双. 向. 公路连接这些城市。第 i 条公路连接城市 ui 和 vi,并有一个级别 wi,表示只允许等级不. 低. 于. wi 的车辆通行。 扶苏会在旅行前提前租一辆车,她希望知道:自己至少要租等级为多少的车,才能 满足:她从该国的任意一个城市出发,都能够通过可以通行的公路访问至少 k 个城市 (包括出发地所在的城市)。 【输入格式】 从文件 travel.in 中读入数据。 本. 题. 单. 个. 测. 试. 点. 内. 有. 多. 组. 测. 试. 数. 据. 。输入第一行是一个整数,表示数据组数 T。对 每组数据,按如下格式输入: 第一行是两个整数,表示城市数量 n 和公路数量 m。 接下来 m 行,每行三个整数 ui , vi , wi 表示一条公路的信息。 接下来一行是一个整数 k,其含义见题目描述。 【输出格式】 输出到文件 travel.out 中。 对每组数据,输出一行一个整数,表示扶苏需要租的车的最小等级。 【样例 1 输入】 1 1 2 4 4 3 1 2 3 4 3 4 4 5 2 4 5 6 1 4 6 7 2 【样例 1 输出】 1 4 第 6 页 共 10 页 2025 洛谷秋令基础组 第一场 扶苏跑路(travel) 【样例 1 解释】 租一个等级为 4 的车,可以通过连接 1, 2 两座城市的公路和连接 3, 4 两座城市的公 路。这样: • 从城市 1 出发可以访问 1, 2 两座城市。 • 从城市 2 出发可以访问 1, 2 两座城市。 • 从城市 3 出发可以访问 3, 4 两座城市。 • 从城市 4 出发可以访问 3, 4 两座城市。 【样例 2】 见选手目录下的 travel/travel2.in 与 travel/travel2.ans。 这个样例满足 k = n。 【样例 3】 见选手目录下的 travel/travel3.in 与 travel/travel3.ans。 【子任务】 用 M 表示单个测试点内 m 的和。 • 对 15% 的数据,n, m ≤ 10,T ≤ 100。 • 对 35% 的数据,n ≤ 100,M ≤ 1000。 • 对 55% 的数据,n, M ≤ 4000。 • 另有 15% 的数据,k = 2。 • 另有 15% 的数据,k = n。 • 对 100% 的数据,3 ≤ n ≤ m ≤ 4×105,M ≤ 4×105,1 ≤ T ≤ 105,1 ≤ ui , vi ≤ n, 1 ≤ wi ≤ 109,2 ≤ k ≤ n。 数据保证: • 从 1 号城市出发,假设可以通过所有公路,则可以到达 1 ∼ n 的所有城市。 • 一条公路连接的两个城市不同,及 ui = vi。 • 不存在两条不同的公路,满足它们连接的城市相同。 【提示】 数. 据. 千. 万. 条,. 清. 空. 第. 一. 条。. 清. 空. 不. 规. 范,. 爆. 零. 两. 行. 泪。
10-05
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值