NTU-GPLT 题解
L1-1 Issho ni Bando o Kumu
Ave Mujica 在第七集隐含了重组老乐队的想法,如果这件事真的成立了,那么千早爱音(就是你的答案)就相当于打了两年的工而一无所获,这件事也点燃了很多爱音厨的怒火。
这个图片也被制成了表情包,配文字:“你怎么还在这?”
#include <iostream>
using namespace std;
int main() {
cout<<"Chihaya Anon"<<endl;
return 0;
}
L1-2 风暴危机
注意圆周率并没有四舍五入到
#include <iostream>
#include <iomanip>
using namespace std;
int main()
{
int r1, r2;
cin>>r1>>r2;
double pi = 3.141592;
double ans = pi * (r2 * r2 - r1 * r1);
cout<<fixed<<setprecision(6)<<ans<<endl;
return 0;
}
L1-3 少吃点罚时吧!
总提交次数 = 剩余失败提交次数 + 通过题数,其中题目通过就代表交了一个正确的提交。
#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
using namespace std;
#define int long long
signed main()
{
int a,b,c;
cin>>a>>b>>c;
cout<<2*a-b+c<<endl;
return 0;
}
L1-4 电脑密码
由于空格的存在,本题卡 getline。并且注意有些单词之间只有标点没有空格。
Python 选手注意换行符 \r\n 和 \n 的读入区别。
若读入的单词存在大写,直接输出
#include<bits/stdc++.h>
using namespace std;
int main() {
string str;
int flag = 0;
getline(cin,str);
int len = 0;
for (char ch : str) {
if ((ch >= 'a' && ch<='z') || (ch>='A' && ch<='Z')) {
len++;
if(ch<='Z')flag = 1;
} else if (len) {
cout << (flag? 5 : len%10);
flag = 0;
len = 0;
}
}
if (len) {
cout << (flag?5:len%10);
}
return 0;
}
L1-5 帕鲁繁育说
构建一个繁育值数组,对其进行均分后上取整,然后从头扫描一遍数组获得差距最小值即可。
#include<bits/stdc++.h>
using namespace std;
vector<int> p(1000000);
void Task(int TestCase) {
int n;
cin >> n;
for (int i = 1; i <= n; i++) {
cin >> p[i];
}
int x, y;
cin >> x >> y;
int son = 0, diff = 1e9, bp = (p[x] + p[y] + 1) / 2;
for (int i = 1; i <= n; i++) {
if (abs(p[i] - bp) < diff) {
diff = abs(p[i] - bp), son = i;
}
}
cout << son << '\n';
}
int main() {
Task(1);
return 0;
}
L1-6 破解数字锁
暴力枚举
#include<bits/stdc++.h>
using namespace std;
int num[6], correct[6], mis[6];
vector<int> ans;
void Task(int TestCase) {
for(int i = 1;i<=5;i++){
cin>>num[i];
cin>>mis[i];
cin>>correct[i];
}
for(int i = 100; i<=999; i++){
int ge = i%10;
int shi = (i/10)%10;
int bai = i/100;
if(ge==shi || shi==bai || bai==ge)continue;
int flag = 1;
for(int j = 1; j<=5; j++){
int cor = 0;int mi = 0;
if(num[j]%10 == ge)cor++;
else if((num[j]/10)%10 == ge || (num[j]/100) == ge)mi++;
if((num[j]/10)%10 == shi)cor++;
else if(num[j]%10 == shi || (num[j]/100) == shi)mi++;
if((num[j]/100) == bai)cor++;
else if((num[j]/10)%10 == bai || num[j]%10 == bai)mi++;
if(correct[j] != cor || mis[j] != mi)flag=0;
}
if(flag)
ans.push_back(i);
}
cout<<ans.size()<<endl;
for(auto i:ans)cout<<i<<endl;
}
int main() {
Task(1);
return 0;
}
L1-7 好好书写
有一种方法是,可以把每一个单词根据大写/下划线拆开,全部降为小写后分成一组一组地存起来。然后根据输出要求将其一组一组地更改格式并拼接即可。
#include <iostream>
#include <iomanip>
#include <string>
#include <vector>
using namespace std;
vector<string> GetWords(const string &str) {
vector<string> res;
string temp;
for (char ch : str) {
if (('A'<=ch && ch<='Z') || ch == '_') {
if (!temp.empty()) {
res.push_back(temp);
temp.clear();
}
}
if (ch != '_') {
if('A'<=ch && ch<='Z')ch = ch-'A'+'a';
temp+=ch;
}
}
if (!temp.empty()) {
res.push_back(temp);
}
return res;
}
string Camel(const vector<string> &words) {
string res;
res += words.front();
for (int i = 1; i < words.size(); i++) {
string word = words[i];
word[0] = toupper(word[0]);
res += word;
}
return res;
}
string Pascal(const vector<string> &words) {
string res;
for (string word : words) {
word[0] = toupper(word[0]);
res += word;
}
return res;
}
string Snake(const vector<string> &words) {
string res;
res += words.front();
for (int i = 1; i < words.size(); i++) {
res += "_" + words[i];
}
return res;
}
void Task(int TestCase) {
int n;
string type;
cin >> n >> type;
for (int i = 1; i <= n; i++) {
string str;
cin >> str;
vector<string> words = GetWords(str);
if (type == "Camel") {
cout << Camel(words) << '\n';
} else if (type == "Pascal") {
cout << Pascal(words) << '\n';
} else {
cout << Snake(words) << '\n';
}
}
}
int main()
{
Task(1);
return 0;
}
L1-8 远离那片花田
考虑到对于不同情况应该具有不同的飞行格式,我们优先将受蛊惑的平民及其周围的地块标记 N。
由于平民周围的地块都被标记,那么未被标记的地块周围只可能存在小路和花海。
随后检查每一个地块。忽略掉所有带有 N 标记的地块,在其他地块上验证花海的个数即可。八连通搜索。
#include<bits/stdc++.h>
using namespace std;
int vertical[8] = {0,1,1,1,0,-1,-1,-1};
int horizonal[8] = {1,1,0,-1,-1,-1,0,1};
void Task(int TestCase) {
int n,m;
cin >> n >> m;
vector<vector<char>> mp(n+2,vector<char>(m+2,0)), fly(n+2,vector<char>(m+2,0));
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
cin>>mp[i][j];
}
}
for(int i = 0;i<=m+1;i++)mp[0][i] = mp[n+1][i] = 'R';
for(int i = 0;i<=n+1;i++)mp[i][0] = mp[i][m+1] = 'R';
for(int i = 1;i<=n;i++){
for(int j = 1;j<=m;j++){
if (mp[i][j] == 'P'){
fly[i][j] = 'N';
for(int k = 0;k<8;k++)
fly[i+vertical[k]][j+horizonal[k]] = 'N';
}
}
}
for(int i = 1;i<=n;i++) {
for (int j = 1; j <= m; j++) {
if(mp[i][j] == 'F' && fly[i][j]!='N'){
int cnt = 0;
for(int k = 0;k<8;k++)
if (mp[i+vertical[k]][j+horizonal[k]] == 'R')cnt++;
if(cnt >= 4)fly[i][j]='H';
else fly[i][j]='N';
}
if(mp[i][j] == 'R' && fly[i][j]!='N'){
int cnt = 0;
for(int k = 0;k<8;k++)
if (mp[i+vertical[k]][j+horizonal[k]] == 'R')cnt++;
if(cnt >= 4)fly[i][j]='L';
else fly[i][j]='H';
}
}
}
for(int i = 1;i<=n;i++) {
for (int j = 1; j <= m; j++)
cout << fly[i][j];
cout << endl;
}
}
int main() {
Task(1);
return 0;
}
L2-1 什么都可以变成比赛?
本题难点在于快速计算名字到字符串
假设我们希望对字符串
因此,可以枚举
这里取
时间复杂度
本题也可使用动态规划(DP)技巧求解,但这就是 L3 的难度了。如果使用此方法,则时间复杂度可降至
方法一:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
const int INF = 16;
const string zaoly = "Zaoly";
vector<string> zaoly_sub;
bool next_choice(vector<bool>& choice, int n)
{
for (int i = n - 1; i > -1; --i)
{
if (!choice[i])
{
choice[i] = true;
return true;
}
choice[i] = false;
}
return false;
}
bool contain_substring(const string& a, const string& b)
{
int j = 0;
for (int i = 0; i < (int)a.length(); ++i)
{
if (j >= (int)b.length())
break;
if (a[i] == b[j])
++j;
}
return j >= (int)b.length();
}
int get_edit_distance(const string& name)
{
int result = 0;
result = INF;
for (const string& sub : zaoly_sub)
if (contain_substring(name, sub))
result = min(result, (int)name.length() + (int)zaoly.length() - (int)sub.length() * 2);
return result;
}
int main()
{
int n = 0;
vector<bool> choice(zaoly.length());
do
{
string s;
for (int i = 0; i < (int)zaoly.length(); ++i)
if (choice[i])
s.push_back(zaoly[i]);
zaoly_sub.push_back(s);
} while (next_choice(choice, zaoly.length()));
cin >> n;
for (int i = n; i > 0; --i)
{
int edit_distance = 0;
string name;
cin >> name;
edit_distance = get_edit_distance(name);
if (edit_distance == 0)
cout << "Grand";
else if (edit_distance <= 1)
cout << "First";
else if (edit_distance <= 3)
cout << "Second";
else if (edit_distance <= 6)
cout << "Third";
else
cout << "Sorry";
cout << '\n';
}
}
方法二:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
struct Index2D
{
Index2D(int m) : m(m) {}
int operator()(int i, int j) const
{
return i * m + j;
}
int m;
};
const string zaoly = "Zaoly";
int get_edit_distance(const string& name)
{
vector<int> distances((zaoly.length() + 1) * (name.length() + 1));
Index2D idx(name.length() + 1);
for (int i = 0; i < (int)zaoly.length(); ++i)
distances[idx(i + 1, 0)] = i + 1;
for (int j = 0; j < (int)name.length(); ++j)
distances[idx(0, j + 1)] = j + 1;
for (int i = 0; i < (int)zaoly.length(); ++i)
for (int j = 0; j < (int)name.length(); ++j)
if (zaoly[i] == name[j])
distances[idx(i + 1, j + 1)] = distances[idx(i, j)];
else
distances[idx(i + 1, j + 1)] = min(distances[idx(i, j + 1)], distances[idx(i + 1, j)]) + 1;
return distances[idx(zaoly.length(), name.length())];
}
int main()
{
int n = 0;
cin >> n;
for (int i = n; i > 0; --i)
{
int edit_distance = 0;
string name;
cin >> name;
edit_distance = get_edit_distance(name);
if (edit_distance == 0)
cout << "Grand";
else if (edit_distance <= 1)
cout << "First";
else if (edit_distance <= 3)
cout << "Second";
else if (edit_distance <= 6)
cout << "Third";
else
cout << "Sorry";
cout << '\n';
}
}
L2-2 编码与解码
我们将第
不难发现文件可以解压,当且仅当
无法解压的情况,就是图里存在环。于是我们可以 DFS 解压到这个文本时,给其标记 vis[i] = true,解压完毕 vis[i] = false。
递归解压它引用到的文本,如果中途发现引用的节点 vis 为 true 说明出现了环(本质上同,无向图找环)。
实时维护一个 string ans 用于存储已经解压的内容,一旦发现其长度超过 ans 即可。
这个字符串题有两个严重坑点:
- 空格的读入。若空格存在,应一个不落地输出。如果
cin入一个字符串,则需要补充一个空格。 - 换行符的读入。题目要求
#结尾,那么前面什么字符都可能产生。此时读入换行符需要额外注意,正确办法是每次按照字符读入,最后拼接成字符串。
#include <bits/stdc++.h>
using namespace std;
const int N = 2e6 + 2;
char s[10][N], ans[N * 10];
int n, len[10], tot;
bool vis[10];
void solve(int num)
{
vis[num] = true;
for (int i = 1; i <= len[num]; ++i)
if (s[num][i] == '*')
{
i++;
if (!vis[s[num][i] - '0'])
{
solve(s[num][i] - '0');
}
else
{
printf("#\n");
exit(0);
}
}
else
ans[++tot] = s[num][i];
if (tot > 1e6)
{
printf("#\n");
exit(0);
}
vis[num] = false;
}
int main()
{
scanf("%d", &n);
for (int i = 1; i <= n; ++i)
{
scanf("\n%c", &s[i][1]);
int j = 1;
while (s[i][j] != '#')
scanf("%c", &s[i][++j]);
len[i] = j - 1;
}
solve(1);
printf(ans + 1);
}
L2-3 朋友悖论
要使第
为了求出每个人的朋友数量,可以枚举边,递增其连接的两个顶点的度数;为了求出每个人所有朋友的朋友数量的最小值,可以枚举边,用其连接的每个顶点的度数分别更新另一个顶点的“朋友的朋友数量的最小值”。
于是,对于所有第
但是这里区间个数可能很多,区间长度也可能很长,因此用暴力法可能超时,必须优化。一种优化的方法是,先维护差分数组,对于每个区间,在左端点加
所有测试用例的总时间复杂度
#include <array>
#include <iostream>
#include <vector>
using namespace std;
void min_update(int& dest, int other)
{
if (other < dest)
dest = other;
}
void test_case()
{
int n = 0, m = 0;
cin >> n >> m;
vector<array<int, 2>> edges(m);
for (int i = 0; i < m; ++i)
{
cin >> edges[i][0] >> edges[i][1];
--edges[i][0];
--edges[i][1];
}
vector<int> friend_ns(n);
for (auto [u, v] : edges)
{
++friend_ns[u];
++friend_ns[v];
}
vector<int> min_friend_ns_among_friends(n, n);
for (auto [u, v] : edges)
{
min_update(min_friend_ns_among_friends[u], friend_ns[v]);
min_update(min_friend_ns_among_friends[v], friend_ns[u]);
}
vector<int> friend_paradox_ns(n + 1);
for (int i = 0; i < n; ++i)
{
if (min_friend_ns_among_friends[i] <= friend_ns[i])
continue;
++friend_paradox_ns[friend_ns[i] + 1];
if (min_friend_ns_among_friends[i] + 1 <= n)
--friend_paradox_ns[min_friend_ns_among_friends[i] + 1];
}
for (int k = 1; k <= n; ++k)
friend_paradox_ns[k] += friend_paradox_ns[k - 1];
for (int k = 1; k <= n; ++k)
{
if (k > 1)
cout << ' ';
cout << friend_paradox_ns[k];
}
cout << '\n';
}
int main()
{
int tests = 0;
cin >> tests;
while (tests > 0)
{
test_case();
--tests;
}
return 0;
}
L2-4 证明修改器
首先,把题目转化成以下问题:
给出某程序一系列变量的声明和使用情况。规定变量在使用前必须声明,已声明的变量不能再次声明。已知程序的行为非法,但只要改动其中一个操作的变量编号,就可以把程序的行为变得合法。请给出一种修改方法,指出修改的操作编号和修改后的新变量编号。
原问题中,公理相当于变量声明;证明语句中的
转化之后,我们可以得出以下结论:
- 涉及重复声明变量的操作最多只有
2 个。如果超过2 个,则不可能只改动一处就能修正。 - 最多只有
1 个变量未声明即使用。如果超过1 个,则不可能只改动一处就能修正。
然后考虑三种情况:
- 有变量重复声明,但没有变量未声明即使用。
- 有变量未声明即使用,但没有变量重复声明。
- 有变量重复声明,也有变量未声明即使用。
对于有变量重复声明,但没有变量未声明即使用的情况,只要改动之后的声明操作,把声明的变量改成在整个程序中都未声明的任意变量即可。
对于有变量未声明即使用,但没有变量重复声明的情况,检查以下两个条件是否同时满足:
- 在第一个未声明的使用操作之前有声明操作,满足其声明的变量在整个程序中都未使用过。
- 未声明即使用的变量在整个程序中都未声明。
如果有,则改动那个声明操作,把声明的变量改成未声明即使用的变量即可。如果没有,则说明未声明的使用操作只有一个(否则无法只改动一处就修正),把未声明即使用的变量改成先前已声明的任意变量即可(注意要满足原问题的要求“
对于有变量重复声明,也有变量未声明即使用的情况,选择其中一个声明操作,把声明的变量改成未声明即使用的变量即可。选择哪个操作来改呢?如果在两个声明操作之间没有未声明的使用操作,则选择后一个操作即可。如果有,则不能选后一个,否则有变量未声明即使用的问题仍未解决;但又必须选一个(否则无法只改动一处就修正),就只能选前一个了。
最后,把结论转化回原问题的结论即可。
时间复杂度
#include <array>
#include <iostream>
#include <vector>
using namespace std;
const int V_MAX = 100'000;
bool proved[V_MAX], used[V_MAX];
int main()
{
int n = 0, m = 0, rp = 0, uwp = 0, uwp_j = 0;
/*
rp = repeated proof
uwp = use without proof
*/
cin >> n;
vector<int> a(n);
for (int i = 0; i < n; ++i)
{
cin >> a[i];
--a[i];
}
cin >> m;
vector<array<int, 3>> b(m);
for (int i = 0; i < m; ++i)
for (int j = 0; j < 3; ++j)
{
cin >> b[i][j];
--b[i][j];
}
rp = -1;
uwp = -1;
for (int i = 0; i < n; ++i)
proved[a[i]] = true;
for (int i = 0; i < m; ++i)
for (int j = 0; j < 3; ++j)
if (j < 2)
{
if (!proved[b[i][j]])
{
uwp = i;
uwp_j = j;
}
else
used[b[i][j]] = true;
}
else
{
if (proved[b[i][j]])
rp = i;
else
proved[b[i][j]] = true;
}
if (rp != -1 && uwp == -1)
{
int unproved = 0;
for (unproved = 0; unproved < V_MAX; ++unproved)
if (!proved[unproved])
break;
cout << rp + 1 << " 3 " << unproved + 1;
}
else if (rp == -1 && uwp != -1)
{
int pwu = 0; // pwu = proof without use
for (pwu = 0; pwu < uwp; ++pwu)
if (proved[b[pwu][2]] && !used[b[pwu][2]])
break;
if (pwu < uwp && !proved[b[uwp][uwp_j]])
cout << pwu + 1 << " 3 " << b[uwp][uwp_j] + 1;
else
{
cout << uwp + 1 << ' ' << uwp_j + 1 << ' ';
if (a[0] != b[uwp][1 - uwp_j])
cout << a[0] + 1;
else
cout << a[1] + 1;
}
}
else
{
int pp = 0; // pp = previous proof
bool pu = false; // pu = previously used
for (pp = 0; pp < rp; ++pp)
if (b[pp][2] == b[rp][2])
break;
for (int i = pp + 1; i <= rp; ++i)
if (b[i][0] == b[uwp][uwp_j] || b[i][1] == b[uwp][uwp_j])
{
pu = true;
break;
}
if (!pu)
cout << rp + 1;
else
cout << pp + 1;
cout << " 3 " << b[uwp][uwp_j] + 1;
}
cout << '\n';
}
L3-1 最小大小数
注意到原数也是原数的一个“大小数”,所以答案一定不会超过原数。又因为原数不超过
若直接枚举,则所有测试用例的时间复杂度是
令
当
其中,第一项
需要注意整数的乘法和乘方运算不要溢出。如果计算结果会超过原数,则应中止计算。
所有测试用例的总时间复杂度
如果想要进一步优化运行时间,则需要更深的观察。注意到,表达式中不能出现底数和指数都至少为三位数的情况,否则计算出的结果必然超过原数。也就是说,底数和指数至少有一个不超过两位数。因此,只需枚举以下两种情况即可:
- 一种是底数不超过两位数,即
0 \le j \le i - 2 ,\max(j + 1, i - 2) \le k \le i - 1 的情况; - 另一种是指数不超过两位数,即
0 \le j \le i - 2 ,j + 1 \le k \le \min(j + 2, i - 1) 的情况。
如果使用此方法,则所有测试用例的总时间复杂度可降至
方法一:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using LL = long long;
const LL INF = 1'000'000'000'000'000'000;
LL concat_integer(const vector<LL>& a, LL from, LL to)
{
LL mult = 0, result = 0;
mult = 1;
for (LL i = from; i < to; ++i)
{
result += a[i] * mult;
mult *= 10;
}
return result;
}
LL powi(LL base, LL exp)
{
LL result = 0;
if (base <= 1)
return base;
result = 1;
while (exp > 0)
{
if (result >= INF / base)
return INF;
result *= base;
--exp;
}
return result;
}
LL multiply(LL x, LL y)
{
if (x >= INF / y)
return INF;
return x * y;
}
void min_update(LL& target, LL value)
{
if (value < target)
target = value;
}
void test_case()
{
LL n = 0;
cin >> n;
string a_str;
cin >> a_str;
vector<LL> a(n);
for (LL i = 0; i < n; ++i)
a[i] = a_str[n - i - 1] - '0';
vector<LL> dp(n + 1);
dp[0] = 1;
for (LL i = 1; i <= n; ++i)
{
dp[i] = concat_integer(a, 0, i);
for (LL from = 0; from <= i - 2; ++from)
for (LL to = from + 1; to <= i - 1; ++to)
min_update(dp[i], multiply(powi(concat_integer(a, to, i), concat_integer(a, from, to)), dp[from]));
}
cout << dp[n] << '\n';
}
int main()
{
LL tests = 0;
cin >> tests;
while (tests > 0)
{
test_case();
--tests;
}
}
方法二:
#include <iostream>
#include <string>
#include <vector>
using namespace std;
using LL = long long;
const LL INF = 1'000'000'000'000'000'000;
LL concat_integer(const vector<LL>& a, LL from, LL to)
{
LL mult = 0, result = 0;
mult = 1;
for (LL i = from; i < to; ++i)
{
result += a[i] * mult;
mult *= 10;
}
return result;
}
LL powi(LL base, LL exp)
{
LL result = 0;
if (base <= 1)
return base;
result = 1;
while (exp > 0)
{
if (result >= INF / base)
return INF;
result *= base;
--exp;
}
return result;
}
LL multiply(LL x, LL y)
{
if (x >= INF / y)
return INF;
return x * y;
}
void min_update(LL& target, LL value)
{
if (value < target)
target = value;
}
void test_case()
{
LL n = 0;
cin >> n;
string a_str;
cin >> a_str;
vector<LL> a(n);
for (LL i = 0; i < n; ++i)
a[i] = a_str[n - i - 1] - '0';
vector<LL> dp(n + 1);
dp[0] = 1;
for (LL i = 1; i <= n; ++i)
{
dp[i] = concat_integer(a, 0, i);
for (LL from = 0; from <= i - 2; ++from)
for (LL to = max(from + 1, i - 2); to <= i - 1; ++to)
min_update(dp[i], multiply(powi(concat_integer(a, to, i), concat_integer(a, from, to)), dp[from]));
for (LL from = 0; from <= i - 2; ++from)
for (LL to = from + 1; to <= min(i - 1, from + 2); ++to)
min_update(dp[i], multiply(powi(concat_integer(a, to, i), concat_integer(a, from, to)), dp[from]));
}
cout << dp[n] << '\n';
}
int main()
{
LL tests = 0;
cin >> tests;
while (tests > 0)
{
test_case();
--tests;
}
}