10.27 a.m.小结

T1:问题 A: PAROVI

题目描述

Mirko和 Slavko在玩一个游戏,先由 Mirko 在 1…N中选出几组互质的数。例如当 N=5时,Slavko可以选择 {{1,2},{3,4},{2,5},{3,5},⋯}中的几组。

然后轮到 Slavko。他需要找到一个 x∈[2,n]使得对于每组 {a,b}\都满足以下两个条件之一:

a,b<x

a,b≥x

例如,如果 Mirko选了 {{1,2},{3,4}},那么 x 可以等于 3。

如果 Slavko找不到满足条件的 x 值,则表示 Mirko获得胜利。现在请你求出 Mirko 获胜的不同情况的总数,在对 10^9取模后告诉他。

输入

第一行包含一个整数 N。

输出

第一行输出一个整数,为 Mirko获胜的不同情况的总数对 10^9取模后的值。

样例输入

2

样例输出

1

提示

输入 #2
3
输出 #2
5
输入 #3
4
输出 #3
21
【样例 1 解释】

Slavko只有一种取法 {{1,2}}。

【样例 2 解释】
Slavko 的其中一种取法为 {{1,2},{1,3}}。


【数据范围】
对于 100%的数据,1≤N≤20。

题解

        刚开始我想的是用容斥原理做。毕竟有一点很容易想到,当所有的数大于等于2时,可以满足x=2,而当所有的数都小于n时,可以满足x=n,然后用容斥原理就能够处理出非1的所有Slavko获胜的情况。于是单独考虑同时包含1和n的,然后两个小时过去了,一分没有。

        后来我想了一下,我这样的决策不够完美,第一次就把大多数情况算进去了,导致剩下的就很难办。但是一时半会儿也找不到更好的容斥了,因此考虑用正解dp的思想。这里有个大佬发现了一维dp的解法,我比较菜,只搞懂了二维的解法。

        dp[i][j]表示前i个数对已经覆盖了[1,j]的区间。

        在这里理一下思路。首先n^2预处理出素数数对,然后就不考虑容斥、补集等奇怪的做法了,直接看Mirko获胜条件:如果把两个数看成区间[l,r],那么当一些区间组合在一起覆盖[1,n]时,一定找不到x,自然就获胜

        那么问题就转化成了区间覆盖问题,显然的二维dp。dp的定义上面已经给出。

        由于要保证无后效性,因此考虑直接按右端点升序排列,这样之后就可以不考虑右区间了(或者说需要保证一个性质)。于是cnt*n的枚举每一个区间的可转移的左端点。在这里要注意,不要超过右端点了,否则没有意义。第一步自然是不取这个区间,因此能直接继承上一步的答案。可能存在两种情况:一、区间有交集。二、区间没有交集。

        如果有交集,那么把这个区间加入会使整个区间的右端点增加(也可能恰好相等,但绝不会减小),因此改变的就是dp[i][a[i].r]了,否则只会加入一个没有意义的区间(因为对[1,j]的答案没有贡献,注意本题dp的含义,因此这种情况是存在的),取和不取都一样,因此答案翻倍就行了。(特别注意!一定要贡献答案,因为可能下一步直接一个巨大的区间全部包含了,那么取不取那个区间都会是答案的一部分)。

参考代码

#include<cstdio>
#include<algorithm>
#define mod 1000000000
#define LL long long
using namespace std;
struct node
{
	int l,r;
}a[500];
int n,cnt=0;
LL dp[500][30];
bool comp1(node p,node q)
{
	return p.r<q.r;
}
int gcd(int p,int q) { return q==0?p:gcd(q,p%q); }
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;i++)
	{
		for(int j=i+1;j<=n;j++)
		{
			if(gcd(i,j)==1)
			{
				cnt++;
				a[cnt].l=i;
				a[cnt].r=j;
			}
		}
	}
	sort(a+1,a+cnt+1,comp1);
	dp[0][1]=1;
	for(int i=1;i<=cnt;i++)
	{
		for(int j=1;j<=a[i].r;j++)
		{
			dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
			if(a[i].l<=j) 
			dp[i][a[i].r]=(dp[i][a[i].r]+dp[i-1][j])%mod;
			else dp[i][j]=(dp[i][j]+dp[i-1][j])%mod;
		}
	}
	printf("%lld",dp[cnt][n]);
	return 0;
}

T2:问题 B:GEPPETTO

题目描述

Geppetto 开了一家披萨店,他正在努力做出全市最好的披萨。

Geppetto 用 N 种原材料做比萨,每种原材料只有一个。原材料标号为 1 到 N。做披萨很简单,只要把原材料混合好然后放进烤箱里烤一烤就行了。但 Geppetto 发现一共有 M 对原材料是冲突的,如果一对冲突的原材料混合在一份披萨里,这份披萨就会变得十分难吃。这给他带来了额外的麻烦。

Geppetto 想知道他最多能做多少种不同的比萨。如果一份比萨上有编号为 i的原材料,而另一份比萨上没有,那么这两份比萨就是不同的。

输入

第一行两个整数 N,M,分别表示原材料总数和冲突总数。

接下来 M 行,每行两个整数 xi,yi,表示一对冲突中两种原材料的编号。

输出

一行一个整数,表示 Geppetto 最多能做多少种披萨。

样例输入

3 2

1 2

2 3

样例输出

5

提示

输入 #2
3 0
输出 #2
8
输入 #3
3 3
1 2
1 3
2 3
输出 #3
4

说明/提示

【样例 1 解释】
Geppetto 可以做出以下 4 种披萨:
1
2
3
1 3
不过因为 Geppetto 可以不放原材料,所以最多可以做出 5 种披萨。
【样例 2 解释】
没有原材料冲突,所以一共可以做出 2^3=8 种披萨。
【样例 3 解释】
由于所有原材料都互相冲突,所以 Geppetto 只能放一种原材料或者不放原材料,一共可以做出 1+3=4种披萨。
【数据范围】
对于 100%的数据,1≤N≤20,1≤M≤400,1≤xi,yi≤N,保证 xi≠yi。

题解

        这道题妥妥的暴力。

        显然可以枚举出所有情况,然后挨个判断是否合法即可。

        我用了二进制压缩,大概快一些。

参考代码

#include<cstdio>
using namespace std;
int n,cnt=0,m,a[50000];
int vis[50000],prt,ans=0;
bool charge()
{
	for(int i=1;i<=cnt;i++)
	{
		if(((prt^a[i])&a[i])==0) return false;
	}
	return true;
}
void dfs(int k)
{
	if(k==n+1)
	{
		if(charge()) ans++;
		return ;
	}
	prt|=(1<<(k-1));
	dfs(k+1);
	prt^=(1<<(k-1));
	dfs(k+1);
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=m;i++)
	{
		int u,v;
		scanf("%d%d",&u,&v);
		if(vis[u*30+v]||vis[u+v*30]) continue;
		vis[u*30+v]=1;
		u--;v--;
		a[++cnt]=(1<<u)^(1<<v);
	}
	dfs(1);
	printf("%d",ans);
	return 0;
}

T3:问题 C:SAVEZ

题目描述

有一个秘密行星 S4 居住着一种奇特的动物,它们的学名是 Loda。Savez 协会派出了一个由 Henrik 将军领导的小组来研究 Loda。Henrik 发现,Loda 有心灵传输的能力,他想在他的军队里雇佣他们。

一只 Loda 由 N 个字符串组成,其中第 i个字符串记为 xi。研究表明,Loda 能进行的心灵传输次数取决于组成它的字符串的一个特殊子序列(不一定是连续的)。字符串 xi 和 xj (i<j) 都可以在该子序列中,当且仅当字符串 xj 以 xi 开头并以 xi结尾。一只 Loda 可以进行的心灵传输次数是组成它的字符串的合法的最长子序列的长度,而你就需要确定它可以进行心灵传输的次数。

输入

第一行一个整数 N,表示组成某一只 Loda 的字符串总数。

接下来 N 行,每行一个仅由大写英文字母构成的字符串 xi,表示构成这一只 Loda 的字符串。

输出

一行一个整数,表示这只 Loda 可以进行心灵传输的次数。

样例输入

5

A

B

AA

BBB

AAA

样例输出

3

提示

输入 #2
5

A
ABA
BBB
ABABA
AAAAAB
输出 #2
3
输入 #3
6
A
B
A
B
A
B
输出 #3
3

说明/提示

【样例 1 解释】
一个最长的子序列为 A AA AAA。
【样例 3 解释】
子序列中的字符串允许相等,因此一个最长的子序列为 A A A B B。
【数据范围】
对于 100% 的数据,1≤N≤2×10^6,1≤∣xi∣≤2×10^6,保证 ∑∣xi∣≤10^6。

题解

        这道题也是卡了好久,最后代码倒是挺短。

        题意就是找一个最长上升子序列,dp的方法应该很好理解。关键是看怎么快速判断一个字符串的前缀、后缀都出现过。然后又发现这道题是字符串,因此想到了字典树。只不过这道题既要保证前缀,还要保证后缀,因此一个大佬想出了同时记录前缀、后缀的方式,用26进制压起来作为map的位。由于这道题空间常数巨大(之前没改之前洛谷始终115分),所以应该用unordered_map代替普通的结构体数组。然后怎么更快,自然是map中套map(个人觉得这种方式很优秀),就直接把RE优化成了18MB的好成绩。综上,这道题就是字典树套dp,同时注意数据结构优化。

参考代码

#include<cstdio>
#include<unordered_map>
#include<cstring>
using namespace std;
int n,t=0,dp[1000005],tot=0,vis[1000005],ans=0;
char s[1000005];
unordered_map< int,unordered_map<int,int> >trie;
int max1(int p,int q) { return p>q?p:q; }
int main()
{
	scanf("%d",&n);
	for(int x=1;x<=n;x++)
	{
		scanf("%s",s);
		dp[x]=1;
		int p=0,len;
		len=strlen(s);
		for(int i=0;i<len;i++)
		{
			dp[x]=max1(dp[x],dp[vis[p]]+1);
			int pt=(s[i]-'A')*26+(s[len-i-1]-'A');
			if(trie[p].find(pt)==trie[p].end())
			trie[p][pt]=++tot;
			p=trie[p][pt];
		}
		dp[x]=max1(dp[x],dp[vis[p]]+1);
		ans=max1(ans,dp[x]);
		vis[p]=x;	
	}
	printf("%d",ans);
	return 0;
}

T4:问题 D: DRŽAVA

题目描述

一个遥远的国家刚刚举行了选举,新首相当选了。目前,这个国家没有一条公路,所以首相决定用双向公路把国内的城市连接起来,组成县,使国家现代化。

这个国家共有 N 个城市,每个县由一个或多个城市组成,两个城市将位于同一个县,当且仅当使用新建的道路可以从一个城市到达另一个城市。这些城市在二维坐标系中用点来表示,两个城市之间的道路表示为连接两个城市所在点的线段。这条路的长度等于以公里为单位的线段的长度。

该国目前正遭受经济衰退,因此首相决定,由于缺乏预算,他们将不修建超过 D 公里的道路。此外,如果至少有一个县存在一个非空子县(可以包括该县的所有城市),使得子县内的居民总数可以被 K 整除,首相就会高兴。例如,如果 K=4,一个县的城市分别有 3、5、7 个居民,首相就会高兴,因为前两个城市的居民总数等于 8。

请你确定最小的 D 来帮助首相降低成本,使得首相感到高兴。

输入

第一行包含两个整数 N,K。

接下来 N 行,每行三个整数 xi,yi,ki,分别表示这个城市的坐标和这个城市的人口。

输出

一行一个实数,表示最小的D,四舍五入保留三位小数

样例输入

3 3
0 4 4
1 5 1
2 6 1

样例输出

1.414

提示

输入 #2

6 11
0 0 1
0 1 2

1 0 3
1 1 4
5 5 1
20 20 10

输出 #2

5.657

输入 #3
6 5

20 20 9
0 0 3
0 1 1
10 0 1

10 1 6
12 0 3
输出 #3
2.000

说明/提示

【样例 1 解释】

只有当所有城市都在同一个县里首相才能高兴,所以最小的 D 为 1.414。
 

【样例 2 解释】
当前五个城市都在同一个县里首相才能高兴,且此时 D 是最小的,所以最小的 D 为 5.657。
【数据范围】
对于 40% 的数据,1≤N≤10^3;
对于 100%的数据,1≤N≤5×10^4,1≤K≤30,0≤xi,yi,ki≤10^8。

题解

        A完这道题后,我思考了一下,这道题考了什么?个人总结:堆,最小生成树,随机化(伪)。

        首先看到D是求最大值,因此第一眼以为是二分,结果发现根本不好算答案。同时发现数据范围根本不支持建边(直接双超)。然后就有一个定理。

        定理:任意k个数必定能组合成k的倍数。

        证明:现在让k个数排成一个数列,必定能处理出每个数的前缀和,因为只用找k的倍数,即mod k=1,所以如果有两个前缀同时mod k=z,那么直接相减就行找到了。又由于k个数的前缀必然会出现至少两个一样的数,或者一个数本身就是k的倍数,证毕。

        根据这个定理,我们发现只要有大于等于k个城市连接在一起,必然是能拼成k的倍数的。因此问题就转化成如何找一个点周围最近的k-1个点?显然,暴力是不行的,那么既然答案D是在其中的,说明此时的D是最优的。那么如果多加一些点,也是不会影响答案的。故我们考虑直接找周围很多点(根据测试,貌似一种旋转角度8个数都能过!我没转,因此用了30个数)。如何找,难道用x坐标排序?当然是这样,只不过要多找一些点。当然出题人可能会故意卡这种方式,无所谓,随机旋转一个角度就可以了,这样被卡的概率就很小了。

        于是可以建很多条边。现在看我们要求的是什么,是D的最大值。因此最小生成树就可以实现这个。对于每一个点,我们维护一个color表示并查集(其实和求最小生成树差不多),然后还要维护这个并查理里的元素模k的值(听说用二进制更好一些,我就是直接开的二维数组)。其他的东西和最小生成树一样,只要找到模k等于0就输出答案。

        至于时间复杂度,显然,虽然算下来拉满是O(nlogn*k^2),但是根据找最小生成树在原理,只要找到了就直接退出,所以实际效率只是找联通块再乘一个常数的效率(可以近似约等于k)。由于数据不卡,所以不转也行。

参考代码

#include<cstdio>
#include<cmath>
#include<queue>
#include<algorithm>
#define EXP 1e-15
using namespace std;
struct tree
{
	int fi,se;
	double dis;
};
struct node
{
	int x,y,nus;
}a[50100];
int n,k,col[50100][32],cost[50100];
int max1(int p,int q) { return p>q?p:q; }
double min_d(double p,double q) { return p-q<EXP?p:q; }
double figure(int p,int q)
{
	return sqrt((double)(a[p].x-a[q].x)*(a[p].x-a[q].x)+(double)(a[p].y-a[q].y)*(a[p].y-a[q].y));
}
priority_queue<tree>q;
bool operator < (tree l,tree r)
{
	return l.dis-r.dis>EXP;
}
bool comp1(node l,node r)
{ return l.x<r.x; }
void build_tree(int u,int v,double dis)
{
	tree ser;
	ser.fi=u;
	ser.se=v;
	ser.dis=dis;
	q.push(ser);
}
int find(int v)//并查集
{
	return cost[v]==v?v:cost[v]=find(cost[v]);
}
int main()
{
	freopen("drzava.in","r",stdin);
	freopen("drzava.out","w",stdout);
	scanf("%d%d",&n,&k);
	for(int i=1;i<=n;i++)
	{
		scanf("%d%d%d",&a[i].x,&a[i].y,&a[i].nus);	
	}
	sort(a+1,a+n+1,comp1);
	for(int i=1;i<=n;i++)
	{
		col[i][a[i].nus%k]=1;
		cost[i]=i;
	}
        //col表示记录联通块中的模k的取值,cost表示属于的联通块
	for(int i=2;i<=n;i++)
	  for(int j=max1(i-30,1);j<i;j++)
	  	build_tree(j,i,figure(i,j));
	while(!q.empty())
	{
		tree see=q.top();q.pop();
		int num1=find(see.fi),num2=find(see.se);
		double di=see.dis;
		if(cost[num1]==cost[num2]) continue;
                //k^2效率处理联通块的合并
		for(int i=0;i<k;i++)
		{
			if(col[cost[num1]][i]!=1) continue;
			int st1=i;
			for(int j=0;j<k;j++)
			{
				if(col[cost[num2]][j]!=1) continue;
				int st2=j;
				if(st1&&st2)
				{
					if(col[cost[num1]][(st1+st2)%k]) continue;
					col[cost[num1]][(st1+st2)%k]=2;
					if((st1+st2)%k==0)
					{
						printf("%.3f",di);
						return 0;
					}
                                        //判断答案直接输出
				}
			}
		}
                //把num2和num1值等于2(新的)全部加入num1
		for(int i=0;i<k;i++)
		{
			col[cost[num1]][i]|=col[cost[num2]][i];
			if(col[cost[num1]][i]==2)
			col[cost[num1]][i]=1;
		}
		//形成一个联通块
		cost[num2]=num1;
	}
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值