2018NOIP提高组Day1(铺设道路&货币系统&赛道修建)

目录

TIPS:数据包下载(NOI官网)

P5019 铺设道路

题目

题目传送门

思路

一道水题
对于每段区间[l,r]最小值为minn,所有d~i~ (i∈[l,r])-=minn
ans += minn
以每一个为0的d[i]作为分割点递归即可

代码

#include <iostream>
#include <cstdio>
#include <algorithm>
using namespace std;
int read(){
	int re = 0 , sig = 1;
	char c = getchar();
	while(c < '0' || c > '9'){
		if(c == '-')sig = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9'){
		re = (re << 1) + (re << 3) + c - '0';
		c = getchar();
	}
	return re * sig;
}
int n;
int d[100010];
int ans;
void dfs(int l , int r){
//	cout << l << '\t' << r << endl;
	if(l > r)return;
	int minn = (1 << 29) , k = -1;
	for(int i = l ; i <= r ; i++){
		if(minn > d[i])
			minn = d[i] , k = i;
	}
	ans += minn;
	for(int i = l ; i <= r ; i++)
		d[i] -= minn;
	int las = l;
	for(int i = l ; i <= r ; i++){
		if(d[i] == 0){
			dfs(las , i - 1);
			las = i + 1;
		}
	}
	dfs(las , r);
}
int main(){
	n = read();
	for(int i = 1 ; i <= n ; i++)
		d[i] = read();
	dfs(1 , n);
	cout << ans;
	return 0;
}

P5020 货币系统

题目

题目传送门

思路

又一道水题
对于每个a[i]若它可以表示为

\[a[i]=\Sigma a[j]*k(j!=i 且k∈\N) \]

则a[i]在货币系统中可有可无,根据题意,不统计它即可,显然,所有的a[j]都是小于等于a[i]的。因此,我们可以将a从小到大数组排序,用类似无限背包的思想解决:设f[i]表示面值为i的货币是否可以被表示(false不能,true可以)初始化f[0]=true,当f[a[i]]=false时,a[i]不能被系统中的其他货币表示出来,统计a[i] (ans++)并用a[i]跑一边无限背包

代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
int read(){
	int re = 0 , sig = 1;
	char c = getchar();
	while(c < '0' || c > '9'){
		if(c == '-')sig = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9'){
		re = (re << 1) + (re << 3) + c - '0';
		c = getchar();
	}
	return re * sig;
}
int n , sum;
int a[110];
bool f[25010];
int main(){
	int T = read();
	while(T--){
		memset(f , 0 , sizeof(f));
		f[0] = true;
		sum = 0;
		n = read();
		for(int i = 1 ; i <= n ; i++)
			a[i] = read();
		sort(a + 1 , a + n + 1);
		for(int i = 1 ; i <= n ; i++){
			if(f[a[i]])continue;
			sum++;
			for(int j = 0 ; j <= a[n] - a[i] ; j ++)
				if(f[j])
					f[j + a[i]] = true;
		}
		printf("%d\n" , sum);
	}
	return 0;
}

P5021 赛道修建

题目

题目传送门
有点

55分思路:

细看数据,发现我们可以获得55的高分(特殊数据点):
2018NOIP提高组Day1(铺设道路&货币系统&赛道修建)

这些特殊数据的分加起来高达55 (然鹅我只拿了45)

m == 1(树的直径)

显然,就是求树的直径
做法:(简单讲)

  1. 以任意一点为源点,求该点到树上所有点的距离O(n)
  2. 找到距离最大的点,设其为b点
  3. 以b点为源点,求b到树上所有点的距离O(n)
  4. 找到距离b最远的点,距离就是树的直径
  5. 证明:不会(但是好像也不用学会证明)

b~i~=a~i~+1(链)

就是一条链,原问题化为:
在一个一维数列中,把数列划分成m段,使所有段中,和最小的段的和 最大
赤裸裸的二分答案,枚举段的最小和,贪心跑一边即可
贪心;

	int cnt = 0 , sum = 0;
	for(int i = 1 ; i < n ; i++){
		if(edlen[i] + sum < maxn){//edlen为按顺序排列的道路长度,maxn为当前二分的最小段的最大值
			sum += edlen[i];
		}
		else{
			sum = 0;
			cnt++;
		}
	}
	return cnt >= m;//按照当前最小段最大值是否可以划分出m段

a~i~=1(菊花图)

就是这里少了10分
就是菊花图,根据题意,每条赛道最多只能经过2条道路,排序计算即可


	dfs(1);//将链式前向星的边权提取出来,存到edlen
	sort(edlen + 1 , edlen + n , cmp);//大到小排序
	int minn = (1 << 29);
	for(int i = 1 ; i <= m ; i++){
		int tmp = edlen[i] + (2 * m - i + 1 <= n - 1 ? edlen[2 * m - i + 1] : 0);
		if(minn > tmp)
			minn = tmp;
	}
	printf("%d" , minn);

55分代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#define nn 50010
using namespace std;
int read(){
	int re = 0 , sig = 1;
	char c = getchar();
	while(c < '0' || c > '9'){
		if(c == '-')sig = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9'){
		re = (re << 1) + (re << 3) + c - '0';
		c = getchar();
	}
	return re * sig;
}

struct node{
	int to , len , nxt;
}ed[nn * 2];
int head[nn];
void addedge(int u , int v , int len){
	static int top = 1;
	ed[top].len = len , ed[top].to = v;
	ed[top].nxt = head[u] , head[u] = top;
	top++;
	ed[top].len = len , ed[top].to = u;
	ed[top].nxt = head[v] , head[v] = top;
	top++;
}

int n , m;
int edlen[nn];
bool vis[nn];
void work1(){
	int k , maxn;
	int dis[nn];
	queue <int> q;
	memset(dis , -1 , sizeof(dis));
	dis[1] = 0 , q.push(1);
	while(!q.empty()){
		k = q.front() , q.pop();
		for(int i = head[k] ; i ; i = ed[i].nxt){
			if(dis[ed[i].to] == -1){
				dis[ed[i].to] = dis[k] + ed[i].len;
				q.push(ed[i].to);
			}
		}
	}
	maxn = 0;
	for(int i = 1 ; i <= n ; i++)
		if(dis[i] > maxn)
			maxn = dis[i] , k = i;
	
	
	memset(dis , -1 , sizeof(dis));
	dis[k] = 0 , q.push(k);
	while(!q.empty()){
		k = q.front() , q.pop();
		for(int i = head[k] ; i ; i = ed[i].nxt){
			if(dis[ed[i].to] == -1){
				dis[ed[i].to] = dis[k] + ed[i].len;
				q.push(ed[i].to);
			}
		}
	}
	maxn = 0;
	for(int i = 1 ; i <= n ; i++)
		if(dis[i] > maxn)
			maxn = dis[i];
	printf("%d" , maxn);
}
void dfs(int x){
	static int siz = 1;
	vis[x] = true;
	for(int i = head[x] ; i ; i = ed[i].nxt){
		if(!vis[ed[i].to]){
			dfs(ed[i].to);
			edlen[siz] = ed[i].len;
			siz++;
		}
	}
}
bool cmp(int a , int b){return a > b;}
void work2(){
	dfs(1);
	sort(edlen + 1 , edlen + n , cmp);
	int minn = (1 << 29);
	for(int i = 1 ; i <= m ; i++){
		int tmp = edlen[i] + (2 * m - i + 1 <= n - 1 ? edlen[2 * m - i + 1] : 0);
		if(minn > tmp)
			minn = tmp;
	}
	printf("%d" , minn);
}
bool check(int maxn){
	int cnt = 0 , sum = 0;
	for(int i = 1 ; i < n ; i++){
		if(edlen[i] + sum < maxn){
			sum += edlen[i];
		}
		else{
			sum = 0;
			cnt++;
		}
	}
	return cnt >= m;
}
void work3(){
	dfs(1);
	int sum = 0;
	for(int i = 1 ; i < n ; i++)
		sum += edlen[i];
	int l = 1 , r = sum;
	while(l < r){
		int mid = (l + r) / 2;
		if((l + r) & 1)
			mid++;
		if(check(mid))	l = mid;
		else			r = mid - 1;
	}
	printf("%d" , l);
}
void work4(){
	dfs(1);
	int sum = 0;
	for(int i = 1 ; i < n ; i++)
		sum += edlen[i];
	printf("%d" , sum);
}
int main(){
	bool ty1 , ty2;
	ty1 = ty2 = true;
	n = read();	m = read();
	for(int i = 1 ; i < n ; i++){
		int u , v , len;
		u = read() , v = read() , len = read();
		addedge(u , v , len);
		if(u != 1)		ty1 = false;
		if(v != u + 1)	ty2 = false;
	}
	if(m == 1){
		work1();
		return 0;
	}
	if(m == n - 1){
		work4();
		return 0;
	}
	if(ty1){
		work2();
		return 0;
	}
	if(ty2){
		work3();
		return 0;
	}
	return 0;
}

100分思路

建议不要看本蒟蒻的:搞了半天,WA变成了TLE,然后卡常,会用的技巧都用了,结果第6个和第18个还是TLE,无可奈何之下,第6个点求了树的直径,第18个点O2也过了话说O2真好用啊
2018NOIP提高组Day1(铺设道路&货币系统&赛道修建)2018NOIP提高组Day1(铺设道路&货币系统&赛道修建)

讲正事儿
首先是二分,同链的情况,设所有赛车路径中长度最小值为maxn
对于每一个结点x,记以x为端点,x的子树中的某个结点为另一个端点,组成的所有的路径中长度最大且小于(不能等)maxn的值为f[x] (如果大于等于maxn的话直接cnt++就好了,不用记录,原因要看f[]的作用,待会就知道了)
有了f,问题和菊花图有点像了。

  1. 首先,令s~i~为x的某个子结点,则任意f[s~i~]<maxn,即不能单独成为一条合法赛道(原因见f的定义)(我们称长度大于等于maxn的赛道合法);
  2. 但是,若将那些道路延长到x结点,就有可能成为合法赛道,并将这些赛道除去,我们将剩下的不合法赛道长度记录在tmp数组里面;
  3. 我们考虑以x为中转点,将剩下的赛道两两合并,使合并后合法赛道数量最大化
  4. 满足当前合法赛道数量最大的前提下,还要满足在x上方的点(父节点)的合法赛道数量最大化,因此,我们将没有合并的赛道中最长的一个记录在f[x]中,供x的父节点使用
  5. 由于使合法赛道数量最大的方案不止一种,我们又双叒叕考虑二分:将tmp取出,二分tmp的下标mid,若舍去tmp[mid]后合法赛道数量仍然可以到达最大值,就将mid前移(tmp[mid]增大),否则mid后移
  6. 关于如何求最大合法赛道数量的问题:贪心:(tmp已经从大到小排序的前提下)j=siz(siz为tmp的数组大小),i从前向后枚举,j向前找到第一个满足tmp[i]+tmp[j]>=maxn的树,cnt++,j--,当i>=j时结束
  7. 请仔细思考贪心的正确性

100分代码

#include <iostream>
#include <cstdio>
#include <cstring>
#include <queue>
#include <algorithm>
#include <vector>
#define rr register
#define nn 50010
using namespace std;
int read(){
	int re = 0 , sig = 1;
	char c = getchar();
	while(c < '0' || c > '9'){
		if(c == '-')sig = -1;
		c = getchar();
	}
	while(c >= '0' && c <= '9'){
		re = (re << 1) + (re << 3) + c - '0';
		c = getchar();
	}
	return re * sig;
}

struct node{
	int to , len , nxt;
}ed[nn * 2];
int head[nn];
void addedge(int u , int v , int len){
	static int top = 1;
	ed[top].len = len , ed[top].to = v;
	ed[top].nxt = head[u] , head[u] = top;
	top++;
	ed[top].len = len , ed[top].to = u;
	ed[top].nxt = head[v] , head[v] = top;
	top++;
}

int n , m;
int lensum = 0;
bool vis[nn];
int f[nn];
int cnt;

void work1(){//专门解决为第6个点TLE而copy55分的
	int k , maxn;
	int dis[nn];
	queue <int> q;
	memset(dis , -1 , sizeof(dis));
	dis[1] = 0 , q.push(1);
	while(!q.empty()){
		k = q.front() , q.pop();
		for(int i = head[k] ; i ; i = ed[i].nxt){
			if(dis[ed[i].to] == -1){
				dis[ed[i].to] = dis[k] + ed[i].len;
				q.push(ed[i].to);
			}
		}
	}
	maxn = 0;
	for(int i = 1 ; i <= n ; i++)
		if(dis[i] > maxn)
			maxn = dis[i] , k = i;
	
	
	memset(dis , -1 , sizeof(dis));
	dis[k] = 0 , q.push(k);
	while(!q.empty()){
		k = q.front() , q.pop();
		for(int i = head[k] ; i ; i = ed[i].nxt){
			if(dis[ed[i].to] == -1){
				dis[ed[i].to] = dis[k] + ed[i].len;
				q.push(ed[i].to);
			}
		}
	}
	maxn = 0;
	for(int i = 1 ; i <= n ; i++)
		if(dis[i] > maxn)
			maxn = dis[i];
	printf("%d" , maxn);
}

int tmp[nn] , siz = 0;//将tmp存为全局变量以节省空间
bool cmp(int a , int b){return a > b;}
void dfs(rr int x , rr int maxn){
	vis[x] = true;
	for(rr int i = head[x] ; i ; i = ed[i].nxt)//先遍历x的每一个子结点以防止tmp在递归时冲突
		if(!vis[ed[i].to]){
			dfs(ed[i].to , maxn);
		}
	
	siz = 0;
	for(rr int i = head[x] ; i ; i = ed[i].nxt)
		if(!vis[ed[i].to]){
			if(f[ed[i].to] + ed[i].len >= maxn) cnt++;//若此赛道延长到x点后长度到达maxn,直接cnt++
			else tmp[++siz] = f[ed[i].to] + ed[i].len;//否则记录到tmp
		}
	sort(tmp + 1 , tmp + siz + 1 , cmp);//大到小排序
	
	int nowcnt = 0;
	for(rr int i = 1 , j = siz ; i < j ; ++i){//求以x为根的树中最大合法赛道数量
		while(tmp[i] + tmp[j] < maxn)
			--j;
		if(i >= j)
			break;
		++nowcnt;
		--j;
	}
	f[x] = 0;
//二分(不知道能不能像主函数里那个二分那样写,反正当时出错改了就没有再改回去)
	rr int l = 1 , r = siz;
	rr int mid;
	rr int res = 0;
	while(l <= r){
		mid = (l + r) / 2;
		
		rr int cntt = 0;
		rr int i = 1 , j = siz;
		for( ; i < j ; i++){
			if(i == mid)continue;//舍去mid结点求最大合法赛道数量
			while(tmp[i] + tmp[j] < maxn || j == mid)
				--j;
			if(i >= j)
				break;
			++cntt;
			--j;
		}
		if(nowcnt == cntt)	r = mid - 1 , res = mid;
		else l = mid + 1;
	}
	cnt += nowcnt;
	f[x] = tmp[res];
	vis[x] = false;
}
bool check(int maxn){
	memset(f , 0 , sizeof(f));//记得初始化
	memset(vis , 0 , sizeof(vis));
	cnt = 0;
	dfs(1 , maxn);
	return cnt >= m;
}

int main(){
	bool ty1 , ty2;
	ty1 = ty2 = true;
	n = read();	m = read();
	for(int i = 1 ; i < n ; i++){
		int u , v , len;
		u = read() , v = read() , len = read();
		lensum += len;
		addedge(u , v , len);
	}
	if(m == 1){
		work1();
		return 0;
	}
	rr int l = 1 , r = lensum;
	r/=m;
	while(l < r){//大二分
		int mid = (l + r) / 2;
		if((l + r) & 1) mid++;
		if(check(mid))	l = mid;
		else			r = mid - 1;
	}
	printf("%d" , l);
	return 0;
}
上一篇:P1803 凌乱的yyy / 线段覆盖


下一篇:1097E. Egor and an RPG game(Dilworth定理)