CPPU算法&编程协会2021寒假第二次模拟赛部分题解

判断地址类型

背景知识

IP 地址是 IP 协议提供的一种统一的地址格式,它为互联网上的每一个网络和每一台主机分配一个逻辑地址,以此来屏蔽物理地址的差异。

最初设计互联网络时,为了便于寻址以及层次化构造网络,每个IP地址包括两个标识码(ID),即网络 ID 和主机 ID 。同一个物理网络上的所有主机都使用同一个网络ID,网络上的一个主机(包括网络上工作站,服务器和路由器等)有一个主机 ID 与其对应。 IP 地址根据网络ID的不同分为5种类型,A 类地址、B 类地址、C 类地址、 D 类地址和 E 类地址。

类型 范围
A 0.0.0.0 ~ 127.255.255.255
B 128.0.0.0 ~ 191.255.255.255
C 192.0.0.0 ~ 224.255.255.255
D 224.0.0.0 ~ 239.255.255.255
E 240.0.0.0 ~ 255.255.255.255

其中,A类地址中 127.x.x.x 的子段是内网地址的保留段,在本题中,我们使用大写字母 “O” 来标识;其他类型的地址均使用本身的字母标识。

题目描述

Hillle 在一家互联网大厂担任网络管理员,他想让你编写一个程序,帮他统计他的数据集中有多少个 A,B,C,D,E 类地址?

如果你遇到了内网地址段,你无需把它计入 A 类地址中,而需计在 O 类地址下。

此外,Hillle 的数据集中可能存在错误的 ip 地址,你需要过滤出它们并统计数量(用 “W” 标识)。

输入格式

第一行包括一个整数 (n),代表数据集中的 ip 地址数量;
接下来 (n) 行每行表示一个 ip 地址,四个子段以 (.) 隔开,并保证为正整数。

输出格式

请参考样例。

输入样例 1

3
250.106.183.223
226.103.155.98
220.247.34.151

输出样例 1

A : 0
B : 0
C : 1
D : 1
E : 1
O : 0
W : 0

输入样例 2

3
127.0.0.1
256.103.155.98
147.1.34.151

输出样例 2

A : 0
B : 1
C : 0
D : 0
E : 0
O : 1
W : 1

——————————————————————————————————————

纯粹的模拟题,会处理字符串就可以了,具体做法请参考代码~

代码如下

#include <bits/stdc++.h>
using namespace std;
int n,fl,d[4],cnt[7];
string ip;
inline bool work() {
	int t=0; memset(d,0,sizeof(d));
	for (int i=0;i<(int)ip.size();i++)
		if (ip[i]=='.') t++; else d[t]=d[t]*10+ip[i]-48;
	if (d[0]<127) fl=1; else if (d[0]==127) fl=0; 
	else if (d[0]<192) fl=2; else if (d[0]<224) fl=3; 
	else if (d[0]<240) fl=4; else fl=5;
	for (int i=0;i<4;i++) if (d[i]>255) return false;
	return true;
}
int main() {
	cin>>n;
	while (n--) cin>>ip,cnt[work()?fl:6]++;
	printf("A : %d
B : %d
C : %d
D : %d
",cnt[1],cnt[2],cnt[3],cnt[4]);
	printf("E : %d
O : %d
W : %d
",cnt[5],cnt[0],cnt[6]);
	return 0;
} 

解题思路

回文地址

题目描述

Hillle 在一家互联网大厂担任网络管理员,在日常工作中,他喜欢美丽的 IP 地址,美丽的条件如下:

在一行中写出 IP 地址所有数字(去除逗号)所得的字符串为回文串。

例如:地址 12.102.20.121 和 0.3.14.130 是美丽的(字符串 “1210220121” 和 “0314130” 是回文串),

而地址 1.20.20.1 和 100.4.4.1 则不是。

Hillle 希望找到所有具有给定数字集组成的美丽 IP 地址,集合中的每个数字必须在IP地址中至少出现一次,且不能包含任何其他数字。

你能帮助他完成这项工作吗?

注意:所有的 IP 均为 IPv4 协议下的地址。

输出格式

第一行包括一个整数 (n),表示给定集合中的数字数量;
第二行包括 (n) 个整数,即集合本身。

输出格式

第一行包括一个整数,表示使用给定集合可以组成的美丽的 IP 地址的数量。
接下来若干行则按顺序输出这些美丽的 IP 地址。

规则如下:先以 IP 的首段从小大到大排序,若相同则以第二段从小到大排序,第三四段同理。

输入样例 1

6
0 1 2 9 8 7

输出样例 1

6
78.190.209.187
79.180.208.197
87.190.209.178
89.170.207.198
97.180.208.179
98.170.207.189

输入样例 2

1
4

输出样例 2

16
4.4.4.4
4.4.4.44
4.4.44.4
4.4.44.44
4.44.4.4
4.44.4.44
4.44.44.4
4.44.44.44
44.4.4.4
44.4.4.44
44.4.44.4
44.4.44.44
44.44.4.4
44.44.4.44
44.44.44.4
44.44.44.44

——————————————————————————————————————

解题思路

题目大意:给定 (n) 个整数,用它们来组成一个合法的回文 IP 地址,每个整数都要用到

我们可以先无视分割的点,用 dfs 暴力枚举前一半,后一半就可以用回文的性质得到

这里有一个小技巧是可以使用一个二进制数表示集合,例如选用了数字 1,3,5 就可以用 21 (10101) 表示

奇数和偶数个数字组成的 ip 处理方式不同:

奇数:<上一个序列> + <当前遍历到的数> + <倒的上一个序列>

偶数:<上一个序列> + <当前遍历到的数> * 2 + <倒的上一个序列>

接下来需要用三个点把得出的序列划分成四个段,可以直接用三重循环枚举所有可能性(再代码中有一些优化的小技巧)

如果每个段的数值都是非法的话,则可以计入最终答案,并用一个 node 结构体记录每段的值,最终进行一次排序

如果能灵活地运用 vector 容器,则可以节省不少的代码量~

代码如下

#include <bits/stdc++.h>
using namespace std;
int n,tar,ans,v[23];
vector<int> now,pt;
struct node {
        // 由于最后需要按顺序输出,所以要储存 ip 各字段的值并重载
	int p1,p2,p3,p4;
	string pri;
	bool operator < (const node &T) const {
		if (p1!=T.p1) return p1<T.p1;
		else if (p2!=T.p2) return p2<T.p2;
		else if (p3!=T.p3) return p3<T.p3;
		else return p4<T.p4;
	}
};
vector<node> book;
inline int readint() {
	int X=0,w=0; char ch=0;
	while (!isdigit(ch)) w|=ch=='-',ch=getchar();
	while (isdigit(ch)) X=(X<<3)+(X<<1)+(ch^48),ch=getchar();
	return w?-X:X;
}
inline bool check(int s,int t) {
        // 验证 ip 某字段的合法性
	if (!pt[s] && s!=t) return false; // 对于 0 进行特判
	int num=0;
	for (int i=s;i<=t;i++) num=num*10+pt[i];
	if (num>255) return false;
	else return true;
}
inline int sum(int s,int t) {
	int num=0;
	for (int i=s;i<=t;i++) num=num*10+pt[i];
	return num;
}
inline void divide() {
	int m=(int)pt.size();
	for (int i=0;i<min(3,m-3);i++) {
		for (int j=i+1;j<min(6,m-2);j++) {
			for (int k=j+1;k<m-1;k++) {
				// 划分为四个段:[0,i] [i+1,j] [j+1,k] [k+1,m-1]
                                // 再逐一进行合法性判断
				if (check(0,i) && check(i+1,j) && check(j+1,k) && check(k+1,m-1)) {
					node tmp;
					tmp.p1=sum(0,i),tmp.p2=sum(i+1,j),tmp.p3=sum(j+1,k),tmp.p4=sum(k+1,m-1);
					for (int t=0;t<m;t++) {
						tmp.pri+=char('0'+pt[t]);
						if (t==i || t==j || t==k) tmp.pri+='.';
					}
					book.push_back(tmp),ans++;
				}
			}
		}
	}
}
void dfs(int m,int use) {
	if (m>6) return;
	for (int i=1;i<=n;i++) {
		if ((use|(1<<v[i]))==tar) { // 条件为给的集合中的数都用上了
                        // 奇数:<上一个序列> + <当前遍历到的数> + <倒的上一个序列>
                        // rbegin() 和 rend() 是倒序迭代器
			pt.clear(),pt=now,pt.push_back(v[i]);
			pt.insert(pt.end(),now.rbegin(),now.rend());
			divide();
                        // 偶数:<上一个序列> + <当前遍历到的数> * 2 + <倒的上一个序列>
			pt=now,pt.push_back(v[i]);
			pt.insert(pt.end(),pt.rbegin(),pt.rend());
			divide();
		}
		now.push_back(v[i]);
		dfs(m+1,use|(1<<v[i]));
		now.pop_back();
	}
}

int main() {
	n=readint();
	for (int i=1;i<=n;i++) v[i]=readint(),tar|=(1<<v[i]);
        // 两个排序,第一个是为了优化效率,第二个是保证题目要求的输出顺序
	sort(v+1,v+n+1),dfs(1,0),sort(book.begin(),book.end()),cout<<ans<<'
';
	for (int i=0;i<ans;i++) cout<<book[i].pri<<'
';
	return 0;
} 

经营与开发

题目描述

4X 概念体系,是指在 PC 战略游戏中一种相当普及和成熟的系统概念,得名自 4 个同样以 "EX" 为开头的英语单词。
eXplore(探索)
eXpand(拓张与发展)
eXploit(经营与开发)
eXterminate(征服)
—— Wiki

今次我们着重考虑 exploit 部分,并将其模型简化:
你驾驶着一台带有钻头(初始能力值 (w) )的飞船,按既定路线依次飞过n个星球。

星球笼统的分为2类:资源型和维修型。( p 为钻头当前能力值)

  1. 资源型:含矿物质量 (a[i]) ,若选择开采,则得到 (a[i]*p) 的金钱,之后钻头损耗 (k%) ,即 (p=p*(1-0.01k))
  2. 维修型:维护费用 (b[i]) ,若选择维修,则支付 (b[i]*p) 的金钱,之后钻头修复 (c %),即 (p=p*(1+0.01c))
    注:维修后钻头的能力值可以超过初始值(你可以认为是翻修+升级)

请作为舰长的你仔细抉择以最大化收入。

输入格式

第一行包括 (4) 个整数 (n,k,c,w)
以下 (n) 行,每行 (2) 个整数 (type,x)
(type)(1)则代表其为资源型星球,(x) 为其矿物质含量 (a[i])
(type)(2) 则代表其为维修型星球,(x) 为其维护费用 (b[i])

输出格式

一个实数(保留 (2) 位小数),表示最大的收入。

输入样例

5 50 50 10
1 10
1 20
2 10
2 20
1 30

输出样例

375.00

数据范围

对于 (30%) 的数据 (n<=100)
另有 (20%) 的数据 (n<=1000;k=100)
对于 (100%) 的数据 (n<=100000; 0<=k,c,w,a[i],b[i]<=100)
保证答案不超过 (10^9)

——————————————————————————————————————

“依次飞过 (n) 个星球”,第一反应就是动态规划,然后验证下无后效性即可。
这题关键点为状态的设计。

Method 1

(F[i][x]) 表示到达第 (i) 个星球,且钻头能力值为 (x) 的最大收入值。
(x) 因为是实数,所以要取一定的精度。对于数值范围都在 (100) 的本题,(n)(10) 左右的时候毫无压力。
时空复杂度:(O(n*x))(x) 为精度范围。
期望得分:(10-30)

Method 2

(F[i][x][y]) 表示到达第 (i) 个星球,且之前开采过 (x) 次,维修过 (y) 次。
因为本题开采和维修对钻头的影响都是定值。所以钻头能力就是 (w*k^x*c^y)
时空复杂度: (O(n^3))
期望得分:(30)

Method 3

对于 (20\% k=100) 的数据,钻头开采一次就永久损坏了。所以只需记录维修过几次即可。
时空复杂度: (O(n^2))
期望得分:(20)(配合 Method 2 为 (50)

Method 4

与 Method 2 一样的状态设计。但是 (x,y) 的范围不需要与 (n) 相同。因为在随机情况下,开采和维修的次数寥寥无几(结合次幂考虑)。
时空复杂度: (O(n*x*y))(x,y) 为选手选择的范围
期望得分:(30-80)

Method 5

与 Method 2 一样的状态设计,但是使用 BFS 来进行 DP 过程,这样就不会遍历到没有被访问到的状态,
同时选手可以自己加上一些简单的贪心判断来减少状态数量。
时空复杂度: (O(?))
期望得分:(70-100)

Method 6

与前 (5) 种做法截然不同。前 (5) 种做法的最大瓶颈就是“当前钻头能力”,下面我们尝试不存储“当前钻头能力”。
(F[i]) 表示前 (i) 个星球的最优收入。很明显这是不行的,因为当前钻头能力会切实影响到后面的过程,不严谨的说,当前钻头能力有“后效性”。

但是这个当前钻头能力对后程的影响无非就是乘上一个数值。(就好像初始钻头能力为 (w) ,实际上你可以按 (1) 来做,最后再把 (ans) 乘上 (w) )。

正难则反,(F[i]) 表示第 (i o n) 个星球的最优收入,且假设从第 (i) 个星球开始时钻头能力为(1)
换句话说,这样的状态设计,规定了一个参考系。

转移过程就变得简单:如果在第 (i) 个星球开采,那么第 (i+1 o n) 个星球的初始钻头能力就是 (1*(1-0.01k))
换句话说,就是 (F[i+1]*(1-0.01k))
所以 (F[i]=max{F[i+1],F[i+1]*(1-0.01k)+a[i]})

对于维护型星球,大同小异。就系数和代价的正负而已。
观察方程,(F[i]=max{F[i+1],F[i+1]*(1-0.01k)+a[i]})
实际上就是取下 (i+1 o n) 的最值而已,所以这题实际上就成了贪心。

代码如下

#include <bits/stdc++.h>
#define MAXN 100007
using namespace std;
int n,w,t[MAXN],a[MAXN];
double k,c,ans;
int main() {
	scanf("%d%lf%lf%d",&n,&k,&c,&w);
	k=1-0.01*k;c=1+0.01*c;
	for(int i=1;i<=n;i++) scanf("%d%d",&t[i],&a[i]);
	for(int i=n;i;i--) {
		if (t[i]==1) ans=max(ans,ans*k+a[i]);
		else ans=max(ans,ans*c-a[i]);
	}
	printf("%.2lf
",ans*w);
}
原文地址:https://www.cnblogs.com/zhwer/p/14412560.html