P3952 时间复杂度
链接:https://ac.nowcoder.com/acm/contest/265/E
来源:牛客网
题目描述
小明正在学习一种新的编程语言 A++,刚学会循环语句的他激动地写了好多程序并 给出了他自己算出的时间复杂度,可他的编程老师实在不想一个一个检查小明的程序, 于是你的机会来啦!下面请你编写程序来判断小明对他的每个程序给出的时间复杂度是否正确。
A++语言的循环结构如下:
F i x y
循环体
E
然后判断 i 和 y 的大小关系,若 i 小于等于 y 则进入循环,否则不进入。每次循环结束后i都会被修改成 i +1,一旦 i 大于 y 终止循环。 x 和 y 可以是正整数(x 和 y 的大小关系不定)或变量 n。n 是一个表示数据规模的变量,在时间复杂度计算中需保留该变量而不能将其视为常数,该数远大于 100。 E
表示循环体结束。循环体结束时,这个循环体新建的变量也被销毁。
注:本题中为了书写方便,在描述复杂度时,使用大写英文字母 O 表示通常意义下 Θ 的概念。
输入描述:
输入文件第一行一个正整数 t,表示有 t(t≤ 10) 个程序需要计算时间复杂度。
每个程序我们只需抽取其中 F i x y
和E
即可计算时间复杂度。注意:循环结构允许嵌套。
接下来每个程序的第一行包含一个正整数 L 和一个字符串,L 代表程序行数,字符串表示这个程序的复杂度,O(1)
表示常数复杂度,O(n^w)
表示复杂度为 nw,其中 w 是一个小于 100 的正整数(输入中不包含引号),输入保证复杂度只有 O(1)
和 O(n^w)
两种类型。
接下来 L 行代表程序中循环结构中的 F i x y
或者 E
。 程序行若以 F
开头,表示进入一个循环,之后有空格分离的三个字符(串)i x y
,其中 i 是一个小写字母(保证不为 n
),表示新建的变量名,x 和 y 可能是正整数或 n
,已知若为正整数则一定小于 100。 程序行若以 E
开头,则表示循环体结束。
输出描述:
输出文件共 t 行,对应输入的 t 个程序,每行输出Yes
或No
或者ERR
,若程序实际复杂度与输入给出的复杂度一致则输出 Yes
,不一致则输出No
,若程序有语法错误(其中语法错误只有: ①F 和 E 不匹配 ②新建的变量与已经存在但未被销毁的变量重复两种情况),则输出ERR
。
注意:即使在程序不会执行的循环体中出现了语法错误也会编译错误,要输出ERR
。
输入样例#1
8
2 O(1)
F i 1 1
E
2 O(n^1)
F x 1 n
E
1 O(1)
F x 1 n
4 O(n^2)
F x 5 n
F y 10 n
E
E
4 O(n^2)
F x 9 n
E
F y 2 n
E
4 O(n^1)
F x 9 n
F y n 4
E
E
4 O(1)
F y n 4
F x 9 n
E
E
4 O(n^2)
F x 1 n
F x 1 10
E
E
输出样例#1:
Yes
Yes
ERR
Yes
No
Yes
Yes
ERR
说明
第一个程序 i 从1 到 1 是常数复杂度。
第二个程序 x 从 1 到 n 是 n 的一次方的复杂度。
第三个程序有一个 F
开启循环却没有E结束,语法错误。
第四个程序二重循环,n 的平方的复杂度。
第五个程序两个一重循环,n 的一次方的复杂度。
第六个程序第一重循环正常,但第二重循环开始即终止(因为 n 远大于 100,100 大于 4)。
第七个程序第一重循环无法进入,故为常数复杂度。
第八个程序第二重循环中的变量 x 与第一重循环中的变量重复,出现语法错误②,输出 ERR
。
备注:
对于 30% 的数据:不存在语法错误,数据保证小明给出的每个程序的前 L/2 行一定为以 F
开头的语句,第 L/2+1 行至第 L 行一定为以 E
开头的语句,L≤ 10,若 x,y 均为整数,x 一定小于 y,且只有 y 有可能为 n
。
对于 50% 的数据:不存在语法错误,L≤ 100,且若 x,y 均为整数,x 一定小于 y,且只有 y 有可能为 n
。
对于 70% 的数据:不存在语法错误,L≤ 100。
对于 100% 的数据:t≤ 10,L≤ 100。若 x,y 均为整数,x 一定小于 y,且只有 y 有可能为 n
。
解题思路:
循环的时间复杂度取决于最内层的计算次数,即嵌套最深的一层循环的计算次数。
循环的嵌套和括号序列的嵌套类似,所以我们可以借助栈来遍历整个代码序列。
当遇到FOR语句时,将该循环压入栈顶,当遇到END语句时,将栈顶的循环弹出。那么栈中从底到顶的序列就是当前循环从外到内嵌套的序列。
对于每层循环FOR i x y,我们先判断它的计算次数cmp:
x 是 n 时:
y 也是 n,那么循环次数是 O(1);
y 是正整数,由于 n 远大于100,且 x,y 在100以内,所以这个循环一次也不执行;
x 是正整数时:
y 是 n,那么会循环 O(n) 次;
y 是正整数,如果 x≤y,那么会循环 O(1)次,如果 x>y,那么一次也不执行;
然后判断整个循环嵌套序列的计算次数:
如果外层循环中的某一层执行次数是0或者当前循环的执行次数是0,那么当前这层的计算次数就是0;
否则当前这层的循环次数就是上一层循环的执行次数乘以前面判断出的循环次数 cmp;
语法错误有两种:
对于当前循环创建的变量,如果在栈中已经出现过,说明与外面的某一层循环的循环变量重名了,产生语法错误;
如果遍历过程中对空栈执行弹出操作,或者遍历结束后栈不为空,说明FOR语句与END语句不匹配,产生语法错误。
时
时间复杂度分析:
总共有 T 个测试数据,对于每个测试数据,每个循环会进栈一次,出栈一次,每次进栈之前会循环一遍栈中所有元素,判断是否存在变量重名的情况,所以总时间复杂度是 O(TL^2)。
#include <iostream>
#include <algorithm>
#include <sstream>
using namespace std;
typedef pair <char, int> PCI;
const int N = 110;
int tt;
PCI stk[N]; // 栈中存储当前嵌套的所有循环
// first存储每一层的变量名
// second存储到当前这层总共的计算量,如果为-1,表示当前这层无法到达
int get_number(string str) // 将字符串转化成整数
{
int res = 0;
for (auto c: str) res = res * 10 + c - '0';
return res;
}
int get_time(string str) // 提取出str中n的次数
{
if (str == "O(1)") return 0;
int t = str.find('^');
string num = str.substr(t + 1);
num.pop_back();
return get_number(num);
}
bool has(char c) // 判断当前栈中是否已经存在变量c
{
for (int i = 1; i <= tt; i++)
if (stk[i].first == c)
return true;
return false;
}
int get_cmp(string x, string y) // 判断 for (int i = x; i <= y; i ++) 的循环次数是n的多少次方
{
if (x == "n")
{
if (y == "n") return 0;
return -1;
}
if (y == "n") return 1;
int a = get_number(x), b = get_number(y);
if (a <= b) return 0;
return -1;
}
int main()
{
int T;
scanf("%d", &T);
while (T--)
{
int n;
string str;
cin >> n >> str;
int tm = get_time(str);
int max_cmp = 0;
bool error = false;
tt = 0;
string line;
getline(cin, line);
for (int i = 0; i < n; i++)
{
getline(cin, line);
if (!error)
{
if (line == "E")
{
if (tt) tt--;
else error = true;
}
else
{
stringstream sin(line);
string F, i, x, y;
sin >> F >> i >> x >> y;
if (has(i[0])) error = true;
else
{
int cmp = get_cmp(x, y);
if (!tt) stk[++tt] = {
i[0], cmp
};
else
{
int computation = -1; // -1表示当前这层无法到达
if (stk[tt].second >= 0 && cmp >= 0) computation = stk[tt].second + cmp;
stk[++tt] = {
i[0], computation
};
}
max_cmp = max(max_cmp, stk[tt].second);
}
}
}
}
if (tt) error = true;
if (error) puts("ERR");
else if (tm == max_cmp) puts("Yes");
else puts("No");
}
return 0;
}
pair的基本用法总结
1,pair的应用
pair是将2个数据组合成一组数据,当需要这样的需求时就可以使用pair,如stl中的map就是将key和value放在一起来保存。另一个应用是,当一个函数需要返回2个数据的时候,可以选择pair。 pair的实现是一个结构体,主要的两个成员变量是first second 因为是使用struct不是class,所以可以直接使用pair的成员变量。
其标准库类型–pair类型定义在#include 头文件中,定义如下:
类模板:template<class T1,class T2> struct pair
参数:T1是第一个值的数据类型,T2是第二个值的数据类型。
功能:pair将一对值(T1和T2)组合成一个值,
这一对值可以具有不同的数据类型(T1和T2),
两个值可以分别用pair的两个公有函数first和second访问。
定义(构造函数):
pair<T1, T2> p1; //创建一个空的pair对象(使用默认构造),它的两个元素分别是T1和T2类型,采用值初始化。
pair<T1, T2> p1(v1, v2); //创建一个pair对象,它的两个元素分别是T1和T2类型,其中first成员初始化为v1,second成员初始化为v2。
make_pair(v1, v2); // 以v1和v2的值创建一个新的pair对象,其元素类型分别是v1和v2的类型。
p1 < p2; // 两个pair对象间的小于运算,其定义遵循字典次序:如 p1.first < p2.first 或者 !(p2.first < p1.first) && (p1.second < p2.second) 则返回true。
p1 == p2; // 如果两个对象的first和second依次相等,则这两个对象相等;该运算使用元素的==操作符。
p1.first; // 返回对象p1中名为first的公有数据成员
p1.second; // 返回对象p1中名为second的公有数据成员
2,pair的创建和初始化
pair包含两个数值,与容器一样,pair也是一种模板类型。但是又与之前介绍的容器不同;
在创建pair对象时,必须提供两个类型名,两个对应的类型名的类型不必相同
pair<string, string> anon; // 创建一个空对象anon,两个元素类型都是string
pair<string, int> word_count; // 创建一个空对象 word_count, 两个元素类型分别是string和int类型
pair<string, vector<int> > line; // 创建一个空对象line,两个元素类型分别是string和vector类型
当然也可以在定义时进行成员初始化:
pair<string, string> author("James","Joy"); // 创建一个author对象,两个元素类型分别为string类型,并默认初始值为James和Joy。
pair<string, int> name_age("Tom", "18");
pair<string, int> name_age2(name_age); // 拷贝构造初始化
pair类型的使用相当的繁琐,如果定义多个相同的pair类型对象,可以使用typedef简化声明:
typedef pair<string,string> Author;
Author proust("March","Proust");
Author Joy("James","Joy");
变量间赋值:
pair<int, double> p1(1, 1.2);
pair<int, double> p2 = p1; //operator =
3,pair对象的操作
访问两个元素操作可以通过first和sencond访问:
pair<int ,double> p1;
p1.first = 1;
p1.second = 2.5;
cout<<p1.first<<' '<<p1.second<<endl;
//输出结果:1 2.5
string firstBook;
if(author.first=="James" && author.second=="Joy")
firstBook="Stephen Hero";
4,生成新的pair对象
还可以利用make_pair创建新的pair对象:
pair<int, double> p1;
p1 = make_pair(1, 1.2);
cout << p1.first << p1.second << endl;
//output: 1 1.2
int a = 8;
string m = "James";
pair<int, string> newone;
newone = make_pair(a, m);
cout << newone.first << newone.second << endl;
//output: 8 James
5,通过tie获取pair元素值
在某些清况函数会以pair对象作为返回值时,可以直接通过std::tie进行接收。比如:
std::pair<std::string, int> getPreson() {
return std::make_pair("Sven", 25);
}
int main(int argc, char **argv) {
std::string name;
int ages;
std::tie(name, ages) = getPreson();
std::cout << "name: " << name << ", ages: " << ages << std::endl;
return 0;
}
二、stringstream
stringstream是 C++ 提供的另一个字串型的串流(stream)物件,和之前学过的iostream、fstream有类似的操作方式。要使用stringstream, 必须先加入这一行:
#include
stringstream主要是用在將一个字符串分割,可以先用.clear( )以及.str( )將指定字串设定成一开始的內容,再用>>把个別的资料输出。
举个例子:
題目:输入的第一行有一个数字 N 代表接下來有 N 行资料,每一行资料里有不固定个数的整数(最多20个,每行最大200个字元),编程將每行的总和打印出來。
输入:
3
1 2 3
20 17 23 54 77 60
111 222 333 444 555 666 777 888 999
输出:
6
251
4995
代码:
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
string s;
stringstream ss;
int n;
cin >> n;
getline(cin, s); //读取换行
for (int i = 0; i < n; i++)
{
getline(cin, s);
ss.clear();
ss.str(s);
int sum = 0;
while (1)
{
int a;
ss >> a;
if(ss.fail())
break;
sum += a;
}
cout << sum << endl;
}
return 0;
}
三、使用stringstream简化类型转换
C++标准库中的提供了比ANSI C的<stdio.h>更高级的一些功能,即单纯性、类型安全和可扩展性。接下来,我将举例说明怎样使用这些库来实现安全和自动的类型转换。
一个例子:
#include <stdio.h>
int main()
{
int n = 10000;
char s[10];
sprintf(s, "%d", n);
//s中的内容为“10000”
//到目前为止看起来还不错。但是,对上面代码的一个微小的改变就会使程序发生错误
printf("%s\n", s);
sprintf(s, "%f", n);
//错误的格式化符
printf("%s\n", s);
return 0;
}
输出:
在这种情况下,由于错误地使用了 %f 格式化符来替代了%d。因此,s在调用完sprintf()后包含了一个不确定的字符串。要是能自动推导出正确的类型,那不是更好吗?
进入stringstream:
由于n和s的类型在编译期就确定了,所以编译器拥有足够的信息来判断需要哪些转换。库中声明的标准类就利用了这一点,自动选择所必需的转换。而且,转换结果保存在stringstream对象的内部缓冲中。你不必担心缓冲区溢出,因为这些对象会根据需要自动分配存储空间。
库定义了三种类:istringstream、ostringstream和stringstream,分别用来进行流的输入、输出和输入输出操作。另外,每个类都有一个对应的宽字符集版本。简单起见,我主要以stringstream为中心,因为每个转换都要涉及到输入和输出操作。
注意,使用string对象来代替字符数组。这样可以避免缓冲区溢出的危险。而且,传入参数和目标对象的类型被自动推导出来,即使使用了不正确的格式化符也没有危险。
1、string到int的转换
string result = "10000";
int n = 0;
stream << result;
stream >> n; //n等于10000
2.重复利用stringstream对象
如果你打算在多次转换中使用同一个stringstream对象,记住在每次转换前要使用clear()方法。
在多次转换中重复使用同一个stringstream(而不是每次都创建一个新的对象)对象最大的好处在于效率。stringstream对象的构造和析构函数通常是非常耗费CPU时间的。
3.在类型转换中使用模板
你可以轻松地定义函数模板来将一个任意的类型转换到特定的目标类型。例如,需要将各种数字值,如int、long、double等等转换成字符串,要使用以一个string类型和一个任意值t为参数的to_string()函数。to_string()函数将t转换为字符串并写入result中。使用str()成员函数来获取流内部缓冲的一份拷贝。
template<class T>
void to_string(string &result, const T &t)
{
ostringstream oss; //创建一个流
oss << t; //把值传递入流中
result = oss.str(); //获取转换后的字符并将其写入result
}
//这样,你就可以轻松地将多种数值转换成字符串了
to_string(s1, 10.5); //double到string
to_string(s2, 123); //int到string
to_string(s3, true); //bool到string
//可以更进一步定义一个通用的转换模板,用于任意类型之间的转换。函数模板convert()含有两个模板参数out_type和in_value,功能是将in_value值转换成out_type类型:
template<class out_type, class in_value>
out_type convert(const in_value & t)
{
stringstream stream;
stream << t; //向流中传值
out_type result; //这里存储转换结果
stream >> result; //向result中写入值
return result;
}
测试代码:
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
template<class T>
void to_string(string &result, const T &t)
{
ostringstream oss;
oss << t;
result = oss.str();
}
template<class out_type, class in_value>
out_type convert(const in_value & t)
{
stringstream stream;
stream << t;
out_type result;
stream >> result;
return result;
}
int main()
{
//to_string实例
string s1, s2, s3;
to_string(s1, 10.5); //double到string
to_string(s2, 123); //int到string
to_string(s3, true); //bool到string
cout << s1 << endl << s2 << endl << s3 << endl << endl;
//convert()例子
double d;
string salary;
string s = "12.56";
d = convert <double> (s); //d等于12.56
salary = convert <string> (9000.0); //salary等于"9000"
cout << d << endl << salary << endl;
return 0;
}
输出:
4.结论
在过去留下来的程序代码和纯粹的C程序中,传统的<stdio.h>形式的转换伴随了我们很长的一段时间。但是,如文中所述,基于stringstream的转换拥有类型安全和不会溢出这样的特性,使我们有充足得理由去使用。库还提供了另外一个特性—可扩展性。你可以通过重载来支持自定义类型间的转换。
5.一些实例
stringstream通常是用来做数据转换的。相比c库的转换,它更加安全,自动和直接。
例子一: 基本数据类型转换例子 int 转 string
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
stringstream ss;
string s;
int i = 1000;
ss << i;
ss >> s;
cout << s << endl;
return 0;
}
运行结果:
*例子二: 除了基本类型的转换,也支持char 的转换
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
stringstream ss;
char s[10];
ss << 8888;
ss >> s;
cout << s << endl;
return 0;
}
运行结果:
例子三: 再进行多次转换的时候,必须调用stringstream的成员函数.clear()
#include <iostream>
#include <string>
#include <sstream>
using namespace std;
int main()
{
stringstream ss;
int first = 0, second = 0;
ss << "456"; // 插入字符串
ss >> first; //转换成int
cout << first << endl;
ss.clear(); //在进行多次转换前, 必须清除ss
ss << true;
ss >> second;
cout << second << endl;
return 0;
}
运行结果:
运行.clear()结果
没有运行.clear()结果
6.使用误区
如果stringstream使用不当,当心内存出问题。试试下面的代码,运行程序前打开任务管理器,看看内存变化。
复制代码,把 stream.str(""); 那一行的注释去掉,再运行程序,内存就正常了。
看来stringstream似乎不打算主动释放内存( 或许是为了提高效率 ),但如果你要在程序中用同一个流,反复读写大量的数据,将会造成大量的内存消耗,因此这时候,需要适时地清除一下缓冲 ( 用 stream.str("") )。
另外不要企图用 stream.str().resize(0) 或 stream.str().clear() 来清除缓冲,使用它们似乎可以让stringstream的内存消耗不要增长得那么快,但仍然不能达到清除stringstream缓冲的效果(做个实验就知道了,内存的消耗还在缓慢的增长)
#include <iostream>
#include <sstream>
using namespace std;
int main()
{
std::stringstream stream;
string str;
while(1)
{
//clear()这个名字让很多人想当然地认为它会清除流的内容。
//实际上它并不清空任何内容,它只是重置了流的状态标志。
stream.clear();
//去掉下面这行注释,清空stringstream的缓冲,每次循环内存消耗将不再增加。
//stream.str("");
stream << "you see see you";
stream >> str;
// 去掉下面这行注释,看看每次循环,你的内存消耗会增加多少
//cout << "Size of stream = " << stream.str().length() << endl;
}
return 0;
}