2018SCUACM Training 3 二分/背包/并查集

题目链接:2018SCUACM Training 3 二分/背包/并查集

昨天补完上周欠的training 2,今天来补training 3了。


A - Bone Collector

  HDU - 2602

已知N个糖果的重量和价值. 我们有一个口袋, 最多可以装V重量的糖果. 问口袋最多能放多少价值的糖果进去? Input
输入的第一行是T, 表示有T组数据.
每组数据由三行组成.
第一行包含两个整数N和V(N <= 1000, V <= 1000). N表示糖果的个数, V表示口袋的载重.
第二行包含N个整数, 表示每一颗糖果的价值.
第三行包含N个整数, 表示每一颗糖果的重量.
Output

对每一组数据, 输出口袋最终可以放进去糖果的价值.

Sample Input
1
5 10
1 2 3 4 5
5 4 3 2 1
Sample Output
14

最简单的01背包问题。

普通背包问题(物品可任意分割)是一个贪心问题,按性价比从高到低选

但是01背包问题用贪心无法解决

01背包问题是一个经典的DP问题,即动态规划

声明一个数组dp[][],其中dp[i][j]表示的是在第1个到第i个物品中,消耗最多j的容量,可以获得的最大的价值(dp数组的意义不唯一,但状态转移方程都是类似的)

则当我们知道dp[i-1]时,想要知道dp[i],也就是在dp[i-1]中进一步考虑向其中添加第i个物品

故状态转移方程为:dp[i][j] = max(dp[i-1][j], dp[i-1][j-cost[i]] + value[i]);分别对应j的消耗中不拿第i个物品,或拿第i个物品

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 1005
#define maxv 1005
using namespace std;
long long max(long long a, long long b)
{
	return a > b ? a : b;
}
long long cost[maxn],value[maxn],dp[maxn][maxv];
int main()
{
	int T,n,v,i,j;
	scanf("%d",&T);
	while(T--)
	{
		memset(dp,0,sizeof(dp));
		scanf("%d%d",&n,&v);
		for(i = 1; i <= n; i++)
			scanf("%lld",&value[i]);
		for(i = 1; i <= n; i++)
			scanf("%lld",&cost[i]);
		for(i = 1; i <= n; i++)
		for(j = 0; j <= v; j++)
		{
		//dp[i][j]:前i个物品容量最多为j时价值的最大值 
			dp[i][j] = max(dp[i][j], dp[i-1][j]);//不选第i个物品 
			if(j >= cost[i])//如果可以选第i个物品 
				dp[i][j] = max(dp[i][j], dp[i-1][j-cost[i]]+value[i]);
		}
		printf("%lld\n",dp[n][v]);
	}
	return 0;
}

B - The Suspects

  POJ - 1611


严重急性呼吸系统综合症( SARS), 一种原因不明的非典型性肺炎,从2003年3月中旬开始被认为是全球威胁。为了减少传播给别人的机会, 最好的策略是隔离可能的患者。
在Not-Spreading-Your-Sickness大学( NSYSU), 有许多学生团体。同一组的学生经常彼此相通,一个学生可以同时加入几个小组。为了防止非典的传播,NSYSU收集了所有学生团体的成员名单。他们的标准操作程序(SOP)如下:
一旦一组中有一个可能的患者, 组内的所有成员就都是可能的患者。
然而,他们发现当一个学生被确认为可能的患者后不容易识别所有可能的患者。你的工作是编写一个程序, 发现所有可能的患者。
 
Input
输入文件包含多组数据。
对于每组测试数据:
第一行为两个整数n和m, 其中n是学生的数量, m是团体的数量。0 < n <= 30000,0 <= m <=500。
每个学生编号是一个0到n-1之间的整数,一开始只有0号学生被视为可能的患者。
紧随其后的是团体的成员列表,每组一行。
每一行有一个整数k,代表成员数量。之后,有k个整数代表这个群体的学生。一行中的所有整数由至少一个空格隔开。
n = m = 0表示输入结束,不需要处理。
Output对于每组测试数据, 输出一行可能的患者。Sample Input
100 4
2 1 2
5 10 13 11 12 14
2 0 1
2 99 2
200 2
1 5
5 1 2 3 4 5
1 0
0 0
Sample Output
4
1
1

问题要判断最终和0号学生一组的同学数目,典型的并查集问题

并查集,是支持以下两种操作的数据结构:合并 :将两个元素合并为一组。查询:返回两个元素是否为一组

首先,我们认为第i个同学是归属于第i组的,即任意两个同学都是不同组的。

然后我们读入每个小组的数据,不断的合并每两个同学所在的组(而不是合并两个同学,因为他们可能已经归属于某个组)

最后就只要扫一遍数出和0号同学一组的同学数目即可。

其实并查集可以看成是一个森林,合并即在森林的两棵树中连一条边使他们连通,查询即判断两个节点所在的树的根节点是否相同。由于并查集只能判断两个元素是否在同一个集合中,而不能判断一个集合究竟有哪些元素,即访问一棵树时只能由下到上访问,而不能从上到下,故减小树的高度会很大程度上提高查询效率。

如何减小树的高度呢?在并查集中,我们只关心某个节点对应的根节点是哪个,而不会关心其父亲或其爷爷是哪个。所以我们在每次取根节点的操作时,都直接将取到的结点连到根节点上就行了。

#include<cstdio>
#include<cstring>
#include<algorithm>
#define maxn 30005
int s[maxn];
int root(int x)// find the root of s[x]
{
	if(s[x] == x)
		return x;
	return s[x] = root(s[x]);
}
int main()
{
	int n,m,i,k,t,last,cnt;
	while(1)
	{
		scanf("%d%d",&n,&m);
		if(n == 0 && m == 0)//multiple data
			break;
		for(i = 0; i <= n; i++)//initialize
			s[i] = i;
		cnt = 0;
		for(i = 0; i < m; i++)
		{
			scanf("%d",&k);//number of groups
			last = -1;
			while(k--)
			{
				scanf("%d",&t);
				if(last == -1)//the first man of the group
					last = t;
				else
					s[root(t)] = root(last);//unite people of thte group
			}
		}
		t = root(0);
		for(i = 0; i < n; i++)//count the answer
			if(root(i) == t)
				cnt++;
		printf("%d\n",cnt);
	}
	return 0;
}

C - 4 Values whose Sum is 0

  POJ - 2785

The SUM problem can be formulated as follows: given four lists A, B, C, D of integer values, compute how many quadruplet (a, b, c, d ) ∈ A x B x C x D are such that a + b + c + d = 0 . In the following, we assume that all lists have the same size n .
Input
The first line of the input file contains the size of the lists n (this value can be as large as 4000). We then have n lines containing four integer values (with absolute value as large as 2  28 ) that belong respectively to A, B, C and D .
Output
For each input file, your program has to write the number quadruplets whose sum is zero.
Sample Input
6
-45 22 42 -16
-41 -27 56 30
-36 53 -37 77
-36 30 -75 -46
26 -38 -10 62
-32 -54 -6 45
Sample Output
5
Hint
1.Sample Explanation: Indeed, the sum of the five following quadruplets is zero: (-45, -27, 42, 30), (26, 30, -10, -46), (-32, 22, 56, -46),(-32, 30, -75, 77), (-32, -54, 56, 30). 2.consider changing four lists into two lists.

这题是在4个数组中分别取一个数,判断有多少个组合能满足四个数之和为0.

这题的简化版——判断【能否】在4个数组中取一个数,使得他们的和为某个值——在《挑战程序设计竞赛》这本书中详细讨论过,而且在书比较靠前的部分

我们先来讨论简化版的问题。因为暴力枚举4个数组的组合复杂度为O(n^4),基本是不用考虑的方法。有一种优化是:先在3个数组中取一个数,判断要凑出sum剩余的数在第四个数组中是否存在。这时问题就变成了在数组中判断某个数是否存在的问题,就可以考虑排序后二分,则优化到O(n^3logn),但还不够。

基于同样的思想,我们可以枚举两个元素,再在剩余的两个数组中判断所需要的另外两个元素是否存在。显然判断两个数组中是否存在某个数是困难的,所以我们可以预处理出这两个数组可能凑出来的所有的和,这个枚举过程是O(n^2)的,得到新的n*n个元素,然后在这个数组中二分。

这种方法枚举两个数组的和复杂度为O(n^2),二分预处理的排序过程需要O(n²logn),枚举前两个数组的两个元素复杂度为O(n²),单次查询的复杂度为O(logn),故总体复杂度为O(n²logn),是一个比较能接受的量级。

但这题我们不但要判断能否凑出某个和,还要求出凑出这个和的方案数。所以在每次二分时,我们不但要判断在数组中是否存在这个元素,还要对这个元素进行计数。计数可以使用STL库中的upper_bound和lower_bound函数,但这里我采取的是一种比较笨的办法,不过我用的方法并不会在复杂度上劣化这个程序。

#include<cstdio>
#include<algorithm>
#define maxn 4005
using namespace std;
int a[maxn],b[maxn],c[maxn],d[maxn];
int sum_cd[maxn*maxn];
int n;
int bin_search(int num)
{
	int l = 0, r = n*n;
	while(l < r)
	{
		int m = (l+r) / 2;
		if(sum_cd[m] == num)
			return m;
		if(sum_cd[m] < num)
			l = m+1;
		else r = m;
	}
	return -1;
}
int cd_count(int index)
{
	int l, r, key;
	l = r = index;
	key = sum_cd[index];
	while(r+1 < n*n && sum_cd[r+1] == key)
		r++;
	while(l-1 >= 0 && sum_cd[l-1] == key)
		l--;
	return r-l+1;
}
int main()
{
	int i,j,t,cnt;
	while(scanf("%d",&n) != EOF)
	{
		cnt = 0;
		for(i = 0; i < n; i++)
			scanf("%d%d%d%d",&a[i],&b[i],&c[i],&d[i]);
		for(i = 0; i < n; i++)
		for(j = 0; j < n; j++)
			sum_cd[i*n+j] = c[i]+d[j];
		sort(sum_cd,sum_cd+n*n);
		for(i = 0; i < n; i++)
		for(j = 0; j < n; j++)
		{
			t = bin_search(-a[i]-b[j]);
			if(t != -1)
				cnt += cd_count(t);
		}
		printf("%d\n",cnt);
	}
	return 0;
}

D - Wireless Network

  POJ - 2236

南亚发生了一次地震。ACM (Asia Cooperated Medical 亚洲联合医疗队) 已经为膝上型电脑搭建了一个无线网络,但受到了一次不可预知的余震攻击,因此网络中的所有电脑都被破坏了。电脑被逐台修复,网络逐步恢复了工作。由于受到硬件的约束,每台电脑只能与距离它不超过 d 米的其它电脑直接通信。但每台电脑可被看作其它两台电脑的通信中转点,也就是说,如果电脑 A 和电脑 B 可以直接通信,或存在一台电脑 C 既可与 A 也可与 B 通信,那么电脑 A 和电脑 B 之间就能够通信。 

在处理网络修复的过程中,工作人员们在任何一个时刻,可以执行两种操作:维修一台电脑,或测试两台电脑是否能够通信。请您找出全部的测试操作。 
输入
第一行包含了两个整数 N 和 d (1 <= N <= 1001, 0 <= d <= 20000)。此处 N 是电脑的数目,编号从 1 到 N;同时,D 是两台电脑之间能够直接通信的最大距离。接下来的 N 行,每行包含两个整数 xi, yi (0 <= xi, yi <= 10000),表示 N 台电脑的坐标。从第 (N+1) 行到输入结束,是逐一执行的操作,每行包含一个操作,格式是以下两者之一: 
1. "O p" (1 <= p <= N),表示维护电脑 p 。 
2. "S p q" (1 <= p, q <= N),表示测试电脑 p 和 q 是否能够通信。 

输入不超过 300000 行。 
输出
对于每个测试操作,如果两台电脑能够通信,则打印 "SUCCESS";否则,打印 "FAIL"。
示例输入
4 1
0 1
0 2
0 3
0 4
O 1
O 2
O 4
S 1 4
O 3
S 1 4
示例输出
FAIL
SUCCESS

这题需要做的是两个操作:修复计算机;判断两台计算机之间能否通信。

显然是个并查集问题。修复计算机对应在若干个结点之间连一条边,判断能否通信对应判断两个结点的根节点是否相同。

预处理一下任意两台计算机之间的距离,当计算机修复时,将其在范围d内的已修复的所有电脑与之连一条边,之后查询直接检查两个结点的root是否相同就行了。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
#define maxn 1005
#define maxd 20005
using namespace std;
typedef struct
{
	int pre,x,y;
} node;
node a[maxn];
int fix[maxn];
int dist[maxn][maxn];
double dis_squ(node a, node b)
{
	return (a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y);
}
int root(int index)
{
	if(a[index].pre == index)
		return index;
	return a[index].pre = root(a[index].pre);
}
int main()
{
	int n,d;
	memset(dist,0,sizeof(dist));
	memset(fix,0,sizeof(fix));
	scanf("%d%d",&n,&d);
	for(int i = 0; i <= n; i++)
		a[i].pre = i;
	for(int i = 1; i <= n; i++)
		scanf("%d%d",&a[i].x, &a[i].y);
	getchar();
	for(int i = 1; i <= n; i++)
	for(int j = i+1; j <= n; j++)
		dist[i][j] = dist[j][i] = dis_squ(a[i],a[j]);
	char c;
	while(scanf("%c",&c) != EOF)
	{
		if(c == 'O')
		{
			int p;
			scanf("%d",&p);
			while(getchar() != '\n');
			fix[p] = 1;
			for(int i = 1; i <= n; i++)
				if(fix[i] && dist[i][p] <= d*d)
					a[root(i)].pre = root(p);
		}
		else if(c == 'S')
		{
			int p,q;
			scanf("%d%d",&p,&q);
			while(getchar() != '\n');
			printf(root(p) == root(q) ? "SUCCESS\n":"FAIL\n");
		}
	}
	return 0;
}

E - The Frog's Games

  HDU - 4004

青蛙举行了运动会,要求青蛙跳跃小河。河流宽度为 L (1<= L <= 1000000000). 河里会有 n 个石头沿着垂直于河岸的直线排成一排,青蛙以跳到石头上,然后再次跳跃。青蛙最多能够跳 m 次;现在青蛙们想知道他们最少应该有多大的跳跃能力才能够到达河对岸?

Input

多组数据;

每行输入   L,n,m;

接下来输入n个数,代表第 i 个石头距离开始位置的距离,两个石头不可能出现在一起。

Output

输出一个数,代表至少需要的跳跃距离(最小的最大跳跃能力);

Sample Input
6 1 2
2
25 3 3
11 
2
18
Sample Output
4
11

这题是求的“最小的最大跳跃能力”,即求“最小值的最大值”。对于“最小的最大”和“最大的最小”这一类问题,我们常常运用二分来解决,将求值问题转换为判定问题。即判断当最大跳跃能力为mid时,能否满足题目条件。

问题已经转换为判定问题之后,这题就是一个再简单不过的模拟题了。我们只要模拟一下青蛙跳的过程,判断能否到达终点即可。即,当当前位置为cur时,下一步要跳到尽可能接近cur+mid位置的石头上,这样重复m次,如果在m次以内跳到了终点,即mid成立,否则mid不成立。最后输出最小值即可。

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define maxn 500005
#define maxm 500005
using namespace std;
int l,n,m;
int a[maxn];
int isAccess(int dis)//每次跳的最大距离为dis时能否到达终点 
{
	if(dis < a[0])
		return 0;
	int cur = 0;//当前位置 
	int time = m;//跳跃次数 
	while(time--)
		cur = *(upper_bound(a,a+n+1,cur+dis)-1);
	return cur >= l;
}
int main()
{
	int i,mid_dis,max_dis,min_dis,dis;
	while(scanf("%d%d%d",&l,&n,&m) != EOF)
	{
		dis = max_dis = l;//最好的情况,每次跳的距离为l(或大于l),跳一次就能跳到对岸 
		min_dis = 1;//最坏的情况,每次跳的距离为1,跳l次才能跳到对岸 
		for(i = 0; i < n; i++)
			scanf("%d",&a[i]);
		a[n] = l;
		sort(a,a+n+1);
		while(max_dis >= min_dis)
		{
			mid_dis = (max_dis+min_dis)/2;
			if(isAccess(mid_dis) == 0)//每次最多跳mid_dis不能到达终点 
				min_dis = mid_dis+1;//最少也要跳mid_dis+1才能到达 
			else//每次最多跳mid_dis可以到达 
			{
				if(dis > mid_dis)//更新dis 
					dis = mid_dis;
				max_dis = mid_dis-1;//找比dis小的范围内还有没有能到达的 
			}
		}
		printf("%d\n",dis);
	}
	return 0;
}

F - FATE

  HDU - 2159

最近xhd正在玩一款叫做FATE的游戏,为了得到极品装备,xhd在不停的杀怪做任务。久而久之xhd开始对杀怪产生的厌恶感,但又不得不通过杀怪来升完这最后一级。现在的问题是,xhd升掉最后一级还需n的经验值,xhd还留有m的忍耐度,每杀一个怪xhd会得到相应的经验,并减掉相应的忍耐度。当忍耐度降到0或者0以下时,xhd就不会玩这游戏。xhd还说了他最多只杀s只怪。请问他能升掉这最后一级吗?
Input输入数据有多组,对于每组数据第一行输入n,m,k,s(0 < n,m,k,s < 100)四个正整数。分别表示还需的经验值,保留的忍耐度,怪的种数和最多的杀怪数。接下来输入k行数据。每行数据输入两个正整数a,b(0 < a,b < 20);分别表示杀掉一只这种怪xhd会得到的经验值和会减掉的忍耐度。(每种怪都有无数个)Output输出升完这级还能保留的最大忍耐度,如果无法升完这级输出-1。Sample Input
10 10 1 10
1 1
10 10 1 9
1 1
9 10 2 10
1 1
2 2
Sample Output
0
-1
1

这题乍一看是一个背包问题,只是相对01背包问题来说,它每种物品可以拿多个,相对完全背包问题来说,它有拿的总数的限制。

最重要的是,它所求的不是我们获得的最大经验值,而是剩余的最大忍耐值。

按照我们背包问题的dp思路,dp[i][j]代表的是前i只怪在j的忍耐度下能获得的最多的经验,显然这个dp策略是不可行的。所以我们不能把它当做一个背包问题来看。而要改变一下dp策略:dp[i][j]代表在i的忍耐度下,杀j只怪能够获得的最大经验值。这样最满足dp[i][s]>=所需经验值的i即为升级所需的最小忍耐度。

这时我们来研究状态转移方程,在dp[i][j]的状态下,杀一只第k种怪物对应的是i减小b[k],j减小1的状态,而k种怪物都有可能被杀,所以要遍历一遍怪物,即dp[i][j] = max(dp[i][j], dp[i-b[k]][j-1] + a[k]),k需要我们遍历

这题看到答案还是比较简单的,但是自己想的话emmmm,反正我没想出来,就算是现在在回顾总结,不看自己以前的代码也总结不出来。。。果然还是做的太少了啊

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
int n;//升最后一级还需要的经验值
int m;//剩余的忍耐度
int k;//怪的种数
int s;//最多杀的怪的个数
int a[105];//杀掉第i只怪可以获得的经验值
int b[105];//杀掉第i只怪会减少的忍耐度
int dp[105][105];//dp[i][j]表示在i的忍耐度下杀j只怪能得到的最多的经验
int main()
{
	while(scanf("%d%d%d%d",&n,&m,&k,&s) == 4)
	{
		memset(dp,0,sizeof(dp));
		for(int i = 1; i <= k; i++)
			scanf("%d%d",&a[i],&b[i]);
		int flag = 1;
		int ans = -1;
		for(int i = 1; i <= m && flag; i++)//遍历忍耐度
		for(int j = 1; j <= s && flag; j++)//遍历杀的怪物数
		for(int p = 1; p <= k && flag; p++)//遍历k种怪物
		{
			if(i >= b[p])//忍耐度大于第p个怪物的消耗
				dp[i][j] = max(dp[i][j], dp[i-b[p]][j-1]+a[p]);
			if(dp[i][j] >= n)
			{
				ans = m - i;
				flag = 0;
			}
		}
		printf("%d\n",ans);
	}
	return 0;
}

G - 食物链

  POJ - 1182


动物王国中有三类动物A,B,C,这三类动物的食物链构成了有趣的环形。A吃B, B吃C,C吃A。 
现有N个动物,以1-N编号。每个动物都是A,B,C中的一种,但是我们并不知道它到底是哪一种。
有人用两种说法对这N个动物所构成的食物链关系进行描述: 
第一种说法是"1 X Y",表示X和Y是同类。 
第二种说法是"2 X Y",表示X吃Y。 
此人对N个动物,用上述两种说法,一句接一句地说出K句话,这K句话有的是真的,有的是假的。当一句话满足下列三条之一时,这句话就是假话,否则就是真话。 
1) 当前的话与前面的某些真的话冲突,就是假话; 
2) 当前的话中X或Y比N大,就是假话; 
3) 当前的话表示X吃X,就是假话。 
你的任务是根据给定的N(1 <= N <= 50,000)和K句话(0 <= K <= 100,000),输出假话的总数。 
Input
第一行是两个整数N和K,以一个空格分隔。 
以下K行每行是三个正整数 D,X,Y,两数之间用一个空格隔开,其中D表示说法的种类。 
若D=1,则表示X和Y是同类。 
若D=2,则表示X吃Y。
Output
只有一个整数,表示假话的数目。
Sample Input
100 7
1 101 1 
2 1 2
2 2 3 
2 3 3 
1 1 3 
2 3 1 
1 5 5
Sample Output
3

这个题目我第一次见到是在《挑战程序设计竞赛》中见到的,题目难度挺大的,反正我现在也没自信一眼能写出来,甚至稍稍做一点变形也不一定能马上相除策略,当时也是看了很久很久才想清楚思路。

首先每只怪物只能归于3类,我们把第i只怪物属于第j类,1<=i<=n,1<=j<=3,定义为事件P[i][j],则n只怪物一共对应有3n个事件

【同样的i对应的3个事件是互斥的】,【而不同的的事件之间是互相独立的】

我们假设现在有第i1和i2只动物是同类的,也就是说P[i1][j]和P[i2][j]是同时发生的,那我们就可以把这两个事件合并。由于j有3个取值,则合并操作要进行3次

如果第i1只动物要吃第i2只动物,那么就代表P[i1][1]和P[i2][2],P[i1][2]和P[i2][3],P[i1][3]和P[i2][1]这3组事件是分别同时发生的,我们同样只需要合并这3组事件即可。

但是合并之前,我们还要判断这条命令是否和之前的命令冲突。还记得前几行说的“同样的i对应的3个事件是互斥的”吗?这就是我们判断命令是否冲突的唯一标准。如果我们要把两个事件合并,前提就是一个事件在之前没有和另一个事件的互斥事件合并。

比如掷骰子的事件,设A:得到的数小于等于3,B:得到的数是奇数,如果A和B同时发生,即我们要准备合并A和B,那么前提就是A在之前没有和B的互斥事件合并,即之前没有认定过“得到的数是偶数”。

回到开始的问题,在这个问题中,比如说我们要合并P[i1][1]和P[i2][2],那么我们只要保证P[i1][1]之前没有和p[i2][1]以及P[i2][3]合并过即可说明这条命令是有效的。

为什么不用反过来判断P[i1][1]以及P[i1][2]是否和P[i2][1]合并过呢?因为之前说了,每次合并操作都涉及到3个合并,而这3个合并是每次在2个固定的i1和i2中取了两个j来合并,所以如果i1的1个事件与i2的3个事件都没有合并过,那就说明了i1和i2之间没有进行过合并操作。如果这段看不懂的话,不妨加上这两个判断条件,并不会影响最终结果。

分析到这里,我们就知道了:X和Y是同类,对应合并P[X][1]和P[Y][1],P[X][2]和P[Y][2],P[X][3]和P[Y][3]这三个事件。X吃Y,对应合并P[X][1]和P[Y][2],P[X][2]和P[Y][3],P[X][3]和P[Y][1],判断一条命令是否为真,只需要判断即将合并的两个结点是否互斥,也就是判断事件1是否已经与事件2的互斥事件合并过。

我认为我还是尽可能的把这题的思路讲清楚了吧(至少比《挑战》上清楚多了)。如果还不懂我也没办法了。说了那么多也离不开两个操作:合并和判断,所以并查集就不多说了。在程序里没开到二维数组,只是把动物i对应的3个事件写作了i,i+n,i+2n的3个数组单元,除此之外都和以上的思路差不多。既然算法上已经清楚了代码上就简单多了。

不过群里好像有另一种思路的方法emmmm。。。可能因为太懒我也没细看,基本思路还是并查集,好像是在并查集中不但存下了父亲是谁,而且还用数字来储存每个动物与根节点之间的关系。。。没细看所以可能说错了,但我还是贴出来给读者自己研究。。。

我的代码:

#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
int a[150005];
int root(int index)
{
	if(a[index] == index)//index指向a[index],a[index]指向自己,则不需要更新
		return a[index];
	return a[index] = root(a[index]);
}
int main()
{
	int n,k,ans;
	int i,d,x,y;
	scanf("%d%d",&n,&k);
	ans = 0;
	for(i = 0; i <= 3*n; i++)//并查集初始化
		a[i] = i;
	while(k--)
	{
		scanf("%d%d%d",&d,&x,&y);
		if(d != 1 && d != 2)
			ans++;//命令格式错误
		else if(x < 1 || x > n || y < 1 || y > n)
			ans++;//下标错误
		else
		{
			if(d == 1)//x和y是同类
			{
				if(root(x) == root(y+n) || root(x) == root(y+2*n))
					ans++;
				else
				{
					a[root(x)] = root(y);
					a[root(x+n)] = root(y+n);
					a[root(x+2*n)] = root(y+2*n);
				}
			}
			else//d==2,x吃y
			{
				if(root(x) == root(y) || root(x) == root(y+2*n))//若已经有xy同类或x被y吃
					ans++;
				else
				{
					a[root(x)] = root(y+n);
					a[root(x+n)] = root(y+2*n);
					a[root(x+2*n)] = root(y);
				}
			}
		}
	}
	printf("%d\n",ans);
	return 0;
}

群里另一种思路的代码:

#include<cstdio>
const int N=50001;
int p[N],r[N],n;
int findset(int x)
{
	if(x!=p[x])
	{
		int fx=findset(p[x]);
		r[x]=(r[x]+r[p[x]])%3;
		p[x]=fx;
	}
	return p[x];
}
bool Union(int d,int x,int y)
{
	int fx=findset(x),fy=findset(y);
	if(fx==fy)
	{
		if((r[y]-r[x]+3)%3!=d)return 1;
		else return 0;
	}
	p[fy]=fx;
	r[fy]=(r[x]-r[y]+d+3)%3;
	return 0;
}
int main()
{
	int k,count,i,d,x,y;
	scanf("%d%d",&n,&k);
	count=0;
	for(i=1;i<=n;i++)p[i]=i,r[i]=0;
	while(k--)
	{
		scanf("%d%d%d",&d,&x,&y);
		if(x>n||y>n||(x==y&&d==2)){count++;continue;}
		if(Union(d-1,x,y))count++;
	}
	printf("%d\n",count);
	return 0;
}

H - A very hard mathematic problem

  HDU - 4282


 Haoren is very good at solving mathematic problems. Today he is working a problem like this: 
  Find three positive integers X, Y and Z (X < Y, Z > 1) that holds 
   X^Z + Y^Z + XYZ = K 
  where K is another given integer. 
  Here the operator “^” means power, e.g., 2^3 = 2 * 2 * 2. 
  Finding a solution is quite easy to Haoren. Now he wants to challenge more: What’s the total number of different solutions? 
  Surprisingly, he is unable to solve this one. It seems that it’s really a very hard mathematic problem. 
  Now, it’s your turn. 
Input  There are multiple test cases. 
  For each case, there is only one integer K (0 < K < 2^31) in a line. 
  K = 0 implies the end of input. 
   
Output  Output the total number of solutions in a line for each test case. 
Sample Input
9
53
6
0
Sample Output
1
1
0
  

        
  
Hint
9 = 1^2 + 2^2 + 1 * 2 * 2
53 = 2^3 + 3^3 + 2 * 3 * 3

题目就是给定K,问有多少组XYZ满足题目的方程。

解方程的问题,我们也可以想到二分法。因为方程左边的函数,当确定两个变元时,都是关于第三个变元的增函数。单调性确定刚好满足二分的前提。

这时候将哪个东西二分就是我们要考虑的问题了。我们当然希望将范围越大的采用二分,范围小的直接遍历就行了。所以我们要确定的第一个变元就是z,因为z在指数位,对左边函数的值的影响是最大的。

怎么确定z的范围呢?题目已经给出了下界z>1,而z的上界要取决于x和y的取值。当x和y尽可能的小时,z才会变大。因为0<x<y,故取极限情况x=1,y=2,则方程变为了2^Z+2Z+1=K,故2^Z<K,Z<log(2)K,这就是Z的上界了。

因为Z的取值范围比较小,所以采取枚举Z的策略。而X和Y在方程中的地位是等价的,所以只能枚举一个,再二分另一个。

这里采用的是枚举x,枚举x也要有范围,所以要求x的上界。

取极限情况Y=X,,则方程变为2X^Z+X^2*Z=K,所以X^Z<K,故X的上界为K^1/Z

到此为止,我们只需要枚举Z和X,再二分判断是否存在Y满足条件,若存在,则找到了一组解。

对这个问题,还有一个很有效的剪枝,即把Z=2的情况单独列出来,这时候左边变成了一个完全平方式,这个式子的解的个数可以很快求出,不需要进行大量枚举。

#include<cstdio>
#include<cstring>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
int k;
ll ipow(ll a, ll b)
{
	if(b == 0)
		return 1;
	ll x = ipow(a,b/2);
	if(b%2) return x*x*a;
	else return x*x;
}
void solve()
{
	int cnt = 0;
	//z=2时,(x+y)²=k
	if(((int)sqrt(k))*((int)sqrt(k)) == k)
		cnt += (sqrt(k)-1)/2;//x!=y
	//z>=3时
	ll maxz = (ll)(log(k)/log(2));
	for(ll z = 3; z <= maxz; z++)
	{
		ll maxy = (ll)pow(k,1.0/z);
		// printf("maxy=%lld\n",maxy);
		for(ll x = 1; x < maxy; x++)
		{
			ll ly = x+1;//y > x
			ll ry = maxy+1;
			ll t = ipow(x,z);
			while(ly < ry)
			{
				ll mid = (ly+ry)/2;
				// printf("%lld %lld %lld %d\n", x,mid,z,k);
				if(t + ipow(mid,z) + x*mid*z == k)
				{
					cnt++;
					break;
				}
				else if(t +  ipow(mid,z) + x*mid*z < k)
					ly = mid+1;
				else if(t + ipow(mid,z) + x*mid*z > k)
					ry = mid;
			}
		}
	}
	printf("%d\n",cnt);
}
int main()
{
	while(scanf("%d",&k) && k != 0)
		solve();
	return 0;
}






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值