7-2 文件传输
当两台计算机双向连通的时候,文件是可以在两台机器间传输的。给定一套计算机网络,请你判断任意两台指定的计算机之间能否传输文件?
输入格式:
首先在第一行给出网络中计算机的总数 N (2≤N≤104),于是我们假设这些计算机从 1 到 N 编号。随后每行输入按以下格式给出:
I c1 c2
其中I
表示在计算机c1
和c2
之间加入连线,使它们连通;或者是
C c1 c2
其中C
表示查询计算机c1
和c2
之间能否传输文件;又或者是
S
这里S
表示输入终止。
输出格式:
对每个C
开头的查询,如果c1
和c2
之间可以传输文件,就在一行中输出"yes",否则输出"no"。当读到终止符时,在一行中输出"The network is connected."如果网络中所有计算机之间都能传输文件;或者输出"There are k
components.",其中k
是网络中连通集的个数。
输入样例 1:
5
C 3 2
I 3 2
C 1 5
I 4 5
I 2 4
C 3 5
S
输出样例 1:
no
no
yes
There are 2 components.
输入样例 2:
5
C 3 2
I 3 2
C 1 5
I 4 5
I 2 4
C 3 5
I 1 3
C 1 5
S
输出样例 2:
no
no
yes
yes
The network is connected.
代码长度限制
16 KB
时间限制
150 ms
内存限制
64 MB
思路:
并查集的模板题。在此基础上题目要求判断网络是否完全相连,即为集合的个数。集合个数为1说明全部相连了,否则输出集合个数。
另:路径压缩
Code:
#include<bits/stdc++.h>
using namespace std;
int s[10005],h[10005] = {0}; //s为并查集,h[x]为以x为根节点的树的高度
int find_set(int x){//查找根
if(x != s[x]) return find_set(s[x]);//递归一直找到根
return s[x];
}
void union_set(int x,int y){//按秩合并
x = find_set(x);
y = find_set(y);
if(h[x] == h[y]){ //高度相同并到左边
s[y] = x;//s[y]为以y对应的根节点,此步操作是修改y的根节点,让y的根节点变为x的根节点即让y对应x的根节点
h[x] ++ ;//以x为根节点的树的高度 + 1
}
else{ //矮树并高树
if(h[x] < h[y]) s[x] = y;
else s[y] = x;
}
}
int main(){
int n;
cin >> n;
for(int i = 1;i <= n;i ++ )
s[i] = i;//初始化,保证根的一一映射,例如x = 1 对应 s[1] = 1。
while(1){
char ch;
int x,y;
cin >> ch;
if(ch == 'S') break;
cin >> x >> y;
if(ch == 'I')
union_set(x,y);
else{
if(find_set(x) == find_set(y)) cout<<"yes"; //如果在同一集合(根相同)说明连接
else cout << "no";
cout << endl;
}
}
int ans = 0;
for(int i = 1;i <= n;i ++ )
if(find_set(i) == i) //统计合并后根的个数
ans ++ ; //统计集合个数
if(ans == 1) cout << "The network is connected.";
else cout << "There are " << ans << " components.";
return 0;
}
运行结果截图:
7-5 公路村村通
现有村落间道路的统计数据表中,列出了有可能建设成标准公路的若干条道路的成本,求使每个村落都有公路连通所需要的最低成本。
输入格式:
输入数据包括城镇数目正整数N(≤1000)和候选道路数目M(≤3N);随后的M行对应M条道路,每行给出3个正整数,分别是该条道路直接连通的两个城镇的编号以及该道路改建的预算成本。为简单起见,城镇从1到N编号。
输出格式:
输出村村通需要的最低成本。如果输入数据不足以保证畅通,则输出−1,表示需要建设更多公路。
输入样例:
6 15
1 2 5
1 3 3
1 4 7
1 5 4
1 6 2
2 3 4
2 4 6
2 5 2
2 6 6
3 4 6
3 5 1
3 6 1
4 5 10
4 6 8
5 6 3
输出样例:
12
代码长度限制
16 KB
时间限制
400 ms
内存限制
64 MB
思路:
问题转化成,只给定一些相连的顶点和权值,求最小生成树。
kruskal算法,即每次都从图中选择权值最小的边,同时要求选择边不能构成圈,执行n-1次即可。
步骤:1.结构体维护各个边的信息:两个结点,权值。2. 优先队列(权值从小到大)。3.并查集,判断是否构成圈,两个点属于同一集合不能选。
Code:
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e3 + 10;
int s[maxn],h[maxn]; //结点和树高
struct node{
int x;
int y;
int w; //权值
friend bool operator < (node a,node b){//运算符重载
return b.w < a.w;
} //优先队列按权值排序
node(int x,int y,int w):x(x),y(y),w(w){}//结构体赋值,和类相似
};
//并查集
int find_set(int x) {
if(s[x] == x) return x;
return find_set(s[x]);
}
void union_set(int x,int y) {
x = find_set(x);
y = find_set(y);
if(h[x] == h[y]){
s[y] = x;
h[x] ++ ;
}
else{
if(h[x] < h[y]) s[x] = y;
else s[y] = x;
}
}
int main(){
int n,m;
cin >> n >> m;
priority_queue<node>q;//优先队列,保证权值最小的在队顶(此时要用到运算符重载)
for(int i = 0;i < m;i ++ ){
int x0,y0,w0;
cin >> x0 >> y0 >> w0;
node p(x0,y0,w0);//赋值结构体
q.push(p); //结点信息入队
}
int cnt = 0,sum = 0; //k记录当前选择边数,sum记录权值和
for(int i = 1;i <= n;i ++ )
s[i] = i;
memset(h,0,sizeof(h)); //初始化并查集
while(!q.empty()){//如果队不空
node p=q.top();//取出队顶
q.pop();//删除队顶
if(find_set(p.x)!=find_set(p.y)){ //不在同一集合中(连接后不会形成环边)
union_set(p.x,p.y); //合并
sum += p.w;//记录权边和
cnt ++ ; //记录选择的边数
}
if(cnt == n-1) break; //k-1个边退出
}
if(cnt == n-1) cout << sum;
else cout << -1; //没选到k-1个边且队列为空,证明连不通
return 0;
}
运行结果截图:
7-1 堆中的路径
将一系列给定数字依次插入一个初始为空的小顶堆H[]
。随后对任意给定的下标i
,打印从H[i]
到根结点的路径。
输入格式:
每组测试第1行包含2个正整数N和M(≤1000),分别是插入元素的个数、以及需要打印的路径条数。下一行给出区间[-10000, 10000]内的N个要被插入一个初始为空的小顶堆的整数。最后一行给出M个下标。
输出格式:
对输入中给出的每个下标i
,在一行中输出从H[i]
到根结点的路径上的数据。数字间以1个空格分隔,行末不得有多余空格。
输入样例:
5 3
46 23 26 24 10
5 4 3
输出样例:
24 23 10
46 23 10
26 10
代码长度限制
16 KB
时间限制
400 ms
内存限制
64 MB
思路:
逐个插入元素,并且维护最小堆的性质。可选择上滤法。
ps:上滤(percolate up)
上滤一般应用于在一个已经排序好的二叉堆中插入一个新节点。通过上滤,使堆在容纳了新节点后仍能保持原来的堆序。
上滤的主要思想:
首先在堆末新建一个空间,称为空穴(hole),然后比较穴的值和其父节点的值。如果空穴的当前位置满足整个堆的堆序,那么就把要插入的值赋给空穴;否则,交换空穴和父节点的位置。迭代实现上面的过程,直到符合整个堆的堆序为止。从宏观上看,空穴会自下而上 地到达满足堆序的位置,称为上滤(percolate up)。
Code:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m,x;
cin >> n >> m;
int a[n + 1];
a[0] = INT_MIN;//放置哨兵
for(int i = 1;i <= n;i ++ ){ //上滤插入
cin >> x;
int j;
for(j = i;x < a[j / 2];j /= 2) //父节点比儿子大,向上过滤
a[j] = a[j / 2];
a[j] = x;
}
for(int i = 0;i < m;i ++ ){
cin >> x;
for(int j = x;j > 0;j /= 2){
if(j != x) cout << " "; //注意输出格式,第一个没有前导的空格
cout << a[j];
}
if(i != m-1) cout << endl; //行末也不能有多余空行
}
return 0;
}
运行结果截图:
7-3 修理牧场
农夫要修理牧场的一段栅栏,他测量了栅栏,发现需要N块木头,每块木头长度为整数Li个长度单位,于是他购买了一条很长的、能锯成N块的木头,即该木头的长度是Li的总和。
但是农夫自己没有锯子,请人锯木的酬金跟这段木头的长度成正比。为简单起见,不妨就设酬金等于所锯木头的长度。例如,要将长度为20的木头锯成长度为8、7和5的三段,第一次锯木头花费20,将木头锯成12和8;第二次锯木头花费12,将长度为12的木头锯成7和5,总花费为32。如果第一次将木头锯成15和5,则第二次锯木头花费15,总花费为35(大于32)。
请编写程序帮助农夫计算将木头锯成N块的最少花费。
输入格式:
输入首先给出正整数N(≤104),表示要将木头锯成N块。第二行给出N个正整数(≤50),表示每段木块的长度。
输出格式:
输出一个整数,即将木头锯成N块的最少花费。
输入样例:
8
4 5 1 2 1 3 1 1
输出样例:
49
代码长度限制
16 KB
时间限制
400 ms
内存限制
64 MB
思路:
这道题就是建立一个哈夫曼树。注意到wpl等于树的非叶子结点之和。计算wpl时不必建立树,只需要模拟建树的过程:即取出最小的两个数,求和,在存入数组中。这个过程重复n-1次即可。该过程可用优先队列处理,调用模板类priority_queue<int,vector<int>,greater<int> >q;优先队列读者可自行实现。
用mutiset也是可以实现的,在插入的时候也维护了数据序列,multimap在插入时会自动排序,并且是用堆实现的。
注意:如果本题用数组存储,每一次执行循环操作都调用sort排序,时间复杂度是n^2logn,会超时。
Code:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n;
cin >> n;
multiset<int>S; //用multiset升序存储数据
for(int i = 0;i < n;i ++ ){
int x;
cin >> x;
S.insert(x);
}
int ans = 0;
for(int i = 1;i <= n - 1;i ++ ){
int x = *S.begin(),y = *(++S.begin()); //取前两个数
ans += x + y;
S.insert(x + y); //插入
S.erase(S.begin());
S.erase(S.begin()); //删除前两个数
}
cout << ans;
return 0;
}
运行结果截图:
7-4 哈夫曼编码
给定一段文字,如果我们统计出字母出现的频率,是可以根据哈夫曼算法给出一套编码,使得用此编码压缩原文可以得到最短的编码总长。然而哈夫曼编码并不是唯一的。例如对字符串"aaaxuaxz",容易得到字母 'a'、'x'、'u'、'z' 的出现频率对应为 4、2、1、1。我们可以设计编码 {'a'=0, 'x'=10, 'u'=110, 'z'=111},也可以用另一套 {'a'=1, 'x'=01, 'u'=001, 'z'=000},还可以用 {'a'=0, 'x'=11, 'u'=100, 'z'=101},三套编码都可以把原文压缩到 14 个字节。但是 {'a'=0, 'x'=01, 'u'=011, 'z'=001} 就不是哈夫曼编码,因为用这套编码压缩得到 00001011001001 后,解码的结果不唯一,"aaaxuaxz" 和 "aazuaxax" 都可以对应解码的结果。本题就请你判断任一套编码是否哈夫曼编码。
输入格式:
首先第一行给出一个正整数 N(2≤N≤63),随后第二行给出 N 个不重复的字符及其出现频率,格式如下:
c[1] f[1] c[2] f[2] ... c[N] f[N]
其中c[i]
是集合{'0' - '9', 'a' - 'z', 'A' - 'Z', '_'}中的字符;f[i]
是c[i]
的出现频率,为不超过 1000 的整数。再下一行给出一个正整数 M(≤1000),随后是 M 套待检的编码。每套编码占 N 行,格式为:
c[i] code[i]
其中c[i]
是第i
个字符;code[i]
是不超过63个'0'和'1'的非空字符串。
输出格式:
对每套待检编码,如果是正确的哈夫曼编码,就在一行中输出"Yes",否则输出"No"。
注意:最优编码并不一定通过哈夫曼算法得到。任何能压缩到最优长度的前缀编码都应被判为正确。
输入样例:
7
A 1 B 1 C 1 D 3 E 3 F 6 G 6
4
A 00000
B 00001
C 0001
D 001
E 01
F 10
G 11
A 01010
B 01011
C 0100
D 011
E 10
F 11
G 00
A 000
B 001
C 010
D 011
E 100
F 101
G 110
A 00000
B 00001
C 0001
D 001
E 00
F 10
G 11
输出样例:
Yes
Yes
No
No
代码长度限制
16 KB
时间限制
400 ms
内存限制
64 MB
思路:
这道题分两步走:首先计算最优的wpl值。判断目标编码带权路径长与最优带权路径长是否相等。如果不相等肯定不是正确编码。如果相等需要进一步的判断是否是前缀串。
计算wpl:最小的带权路径长用优先队列计算。目标的带权路径长只需要用对应字母的权值×编码长度即可。
判断前缀:只要一个字符串的长度小于一个字符串,将这个小字符串与长字符串前面的做比较。我的处理是用mutimap存入键值对<编码长度,编码字符>,在之后的比较两层for循环即可。如果不事先排序,枚举n*n次,有一个测试点会超时。
本题所有测试点开始给定的字母权值顺序和后面给出的编码顺序是一样的,所以按照顺序对应即可。
ps:
Code:
#include<bits/stdc++.h>
using namespace std;
int main(){
int n,m;
cin >> n;
priority_queue<int,vector<int>,greater<int> >q;//优先队列维护小顶堆
int a[n],f;
char c;
string s;
for(int i = 0;i < n;i ++ ){ //预处理,记录第i个字符对应的权值
cin >> c >> f;
a[i] = f;//统计频率
q.push(f); //权值入队
}
int wpl = 0;
for(int i = 1;i <= n - 1;i ++ ){ //计算wpl
int x = q.top();
q.pop();
int y = q.top();
q.pop();
wpl += x + y;
q.push(x + y);
}
cin >> m;//m套编码
while(m -- ){
multimap<int,string>mp1; //按照长度-字符串存入编码信息
int wpl0 = 0;
for(int i = 0;i < n;i ++ ){
cin >> c >> s;
mp1.insert(make_pair(s.size(),s));
wpl0 += s.size() * a[i];//计算wpl0
}
if(wpl != wpl0) cout << "No";
else{
int flag = 1;
for(multimap<int,string>::iterator it = mp1.begin();it != mp1.end();it ++ ){//判断前缀
multimap<int,string>::iterator it0 = it;
it0 ++ ; //长度 >= *it 的在后面(小顶堆维护有序的作用在此刻发挥)
for(;it0 != mp1.end();it0 ++ ){
string s1 = it0 -> second,s2 = it -> second;
if(s1.find(s2) == 0){ /*也可以写做if(s1.substr(0,s2.size())==s2)*/
flag = 0;//如果返回值为0即为前缀,则不是前缀码,跳出。
break;
}
}
if(flag == 0) break;//一个不是前缀码那么一整套也不是,剩下的不用判断直接跳出,所以使用两层break提高效率。
}
if(flag) cout << "Yes";
else cout << "No";
}
if(m) cout << endl;//注意:末尾无空行
}
return 0;
}
运行结果截图: