我是比赛链接
A(AtCoder):平方不等式
思路(huas_wlq):
直接判断输出
#include /// 万能头文件(囊括了绝大多数的头文件,无需再定义每一个头文件)
using namespace std;
int main(){
int a, b, c;
cin >> a >> b >> c;
if(a * a + b * b < c * c) cout << "Yes";
else cout << "No";
return 0;
}
B(huas_wlq):阅读理解
思路1(huas_wlq):
将数字当字符串处理,统一读入反向输出。
#include
using namespace std;
string s;
int main(){
cin >> s;
for(int i = s.length() - 1; ~i; i --) {
cout << s[i];
}
return 0;
}
思路2(huas_wlq):
通过除、模运算,每次取得数字最后一位输出。
#include
using namespace std;
#define ll long long /// 宏定义
int main() {
ll x;
cin >> x;
while(x) {
/// x 没有前导零
cout << x % 10; /// 取得最后一位数
x /= 10; /// 删除最后一位数
}
return 0;
}
C(huas_wlq):阴阳合同
知识点:
1776 神奇的数组 姐妹篇,基础dp + 思维,关于基础dp 部分请看这篇 题解。
思路(huas_wlq):
考虑数组每个位置的意义,对于dp数组 dp[ i ][ 1] 其意义为“前 i 个数在第 i 个数被选取的时候所能获得的最大值(若数组第二维是 0 则代表的是不选时的最大值),而本题中我们需要考虑中间的每一个数其必须被选取或者必须不被选取时的最大值。
对于一个特殊的指定位置,我们假设其为第 x 位,且其是一定要被选取的,那么其最大值就是 其被选取 且其左右两个位置 不被选取的最大值 的和故有 ans = a[ x ] + dp1[ x - 1 ][ 0 ] + dp2[ x + 1 ][ 0 ] (其中 a[ x ] 代表第 x 个物品的价值 ,而 dp1 数组 代表从前往后求得的当前位置选于不选的最大值, dp2 数组 则是从后往前求得的当前位置选于不选的最大值)。
同理在 x 必定不被选取时有:ans = a[ x ] + dp1[ x - 1 ][ 0 ] + dp2[ x + 1 ][ 0 ] 。
#include
using namespace std;
const int N = 100009; /// 定义数组长度
int n, a[N], m;
int b[N][2], c[N][2];
int main() {
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf("%d", a + i);
for(int i = 1; i <= n; i ++) {
/// dp1
b[i][0] = max(b[i - 1][0], b[i - 1][1]);
b[i][1] = b[i - 1][0] + a[i];
}
for(int i = n; i; i --) {
/// dp2
c[i][0] = max(c[i + 1][0], c[i + 1][1]);
c[i][1] = c[i + 1][0] + a[i];
}
int x, p;
while(m --) {
scanf("%d%d", &x, &p);
if(p) printf("%d\n", c[x][1] + b[x][1] - a[x]);
else printf("%d\n", c[x][0] + b[x][0]);
/// 上面的两个求解式子看起来和题解中的式子不一样,但实际上两种写法的效果是等价的(请读者画图自证)。
}
return 0;
}
D(琉璃花社的猫朵):翻硬币的小游戏
题解(琉璃花社的猫朵):
本题是一个思维题,大致题意如下:
小明摆了一圈硬币,告诉你每个硬币是正面还是反面,然后两个人去翻转硬币,每次翻转都会将连续的处于同一面的所有硬币都翻过来,问你是先手先将所有硬币翻至同一面还是后手先将所有硬币翻至同一面。
对于这个问题,实现我们要知道硬币只有两面,且对于连续的处于同面的硬币我们无论翻的是这连续一段中的哪个位置都会将这一段完全翻转,所以可以做一个转化,即,假设这是一个由红蓝两种颜色构成的环,那么每次
操作就将其中一段颜色变成另一种颜色(如原来是红色的段就变蓝色),那么每次操作都必定会将其操作的那一段变成和它相邻的段一样的颜色,如果操作之后还没有获胜,那么该段就会和它相邻的两段合并成一段,那么整个红蓝相间的环的红蓝段的段数-2,如果整个环只剩下一段红色和一段蓝色那么接下来无论对哪一段进行操作都必定会获胜,所以该题只需要统计出来红蓝段数即可,也就是连续正面和连续反面的段数,接下来由于每次操作必定使段数-2,所以如果段数除以2是奇数,也就意味着先手必定进行最后一次操作,所以先手会获胜,如果段数除以2是偶数则后手会获胜,同时需注意,题目规定了当局面开始时所有硬币都在同一面时算先手获胜,所以对于段数为1时需要特判一下。
所以该题就是判断奇偶的题。
#include
using namespace std;
const int N = 1e4+10;
int a[N];
int cnt;
int main()
{
int n;
int now;
scanf("%d",&n);
for(int i = 0;i<n;i++)
{
scanf("%d",&a[i]);
if(i == 0)now = a[i];//记录第一段的面
else if(now != a[i])
{
cnt++;//统计段数
now = a[i];//记录下一段的面
}
}
if(now != a[0])cnt++;//如果首尾不能合并则段数加1
if(cnt == 0 || (cnt/2)%2)printf("Alan\n");//判断段数除2的结果的奇偶性
else printf("Gino\n");
return 0;
}
E(the_xin):迷宫大师
知识点:
bfs(广度优先搜索)。
思路(huas_wlq):
注意到本题的数据范围比较小,如果枚举出每个点求出距离其最远的点的距离那么就可以通过判断答案的大小关系而得到最终的答案。
而对于一个图来说,得到图中所有可以达到的点到指定点的距离我们可以通过 bfs 来求得,而 bfs 时间复杂度是地图的大小。结合上面提到的枚举每一个点我们可以求得时间复杂度为:T * (n * m) * (n * m) <= 2.43e7 < 2e8,所以暴力枚举搜索的时间复杂度是可以接受的。
#include
using namespace std;
#define ll long long
typedef pair<int, int> pii; /// 声明一个类型(用 pii 代替前面繁琐的命名方式
/// pair 是 C++ 提供的一个存储数对的变量类型,内部重载了很多常用的方法(建议百度了解下)
const int N = 32; /// 数组范围
int n, m;
char a[N][N];
struct node {
/// 用来存储答案
int x; /// 步数
pii one, two; /// 起点的坐标和终点的坐标
int operator < (const node &p) const {
if(x != p.x) return x < p.x; /// 步数越大越好
if(one != p.one) return one > p.one; /// 坐标(字典序)越小越好
return two > p.two;
}
}no;
int amax; /// 存储最大的步长
priority_queue<node> pq; /// 结构体优先队列,用来存储所有的可行解
/// 优先队列是 STL 提供的一种类二叉堆的数据结构,其(默认)保证队头元素一定是队内最大元素,
/// 其插入和删除的时间复杂度都为 log 级别
int vis[N][N]; /// 标记当前坐标走过与否
pii pi;
queue<pii> qu; /// 记录第一次被走过的位置坐标
int tx[] = {
0, 1, 0, -1, 0};
void bfs(int xx, int yy) {
int x, y;
vis[xx][yy] = 1;
qu.push({
xx, yy});
while(!qu.empty()) {
pi = qu.front(); qu.pop();
for(int i = 1; i < 5; i ++) {
x = pi.first + tx[i]; y = pi.second + tx[i - 1];
if(!x || !y || x > n || y > m || a[x][y] == '#' || vis[x][y]) continue; /// 越界或者“撞墙”
vis[x][y] = vis[pi.first][pi.second] + 1;
qu.push({
x, y}); /// 第一次遍历到的结点入队
if(vis[x][y] >= amax) amax = vis[x][y], pq.push({
vis[x][y], {
xx, yy}, {
x, y}});
/// 作为可行答案之一入队,判断是为了删除不可能的解(步数太少),不删会超时。
}
}
}
int main(){
int t; cin >> t;
while(t --) {
cin >> n >> m;
for(int i = 1; i <= n; i ++) scanf(" %s", a[i] + 1);
for(int i = 1; i <= n; i ++){
for(int j = 1; j <= m; j ++) {
/// 枚举每个点
if(a[i][j] == '.') {
/// 从有路的点出发才有意义
bfs(i, j);
memset(vis, 0, sizeof(vis));
}
}
}
no = pq.top(); /// 队头即为最优解
cout << no.one.first << ' ' << no.one.second << ' ' << no.two.first
<< ' ' << no.two.second << ' ' << no.x - 1 << endl;
while(!pq.empty()) pq.pop(); amax = 0; /// 清空队列并重置最长路
}
return 0;
}
F(huaszzh):小孩儿的问题
思路1(huaszzh):
这个数据是 1e12 ,我们直接暴力会超时,此时我们只需要将数字拼接起来的话就能做到 1e6 的效果,这样我们就只需要暴力枚举1e6即可。(时间复杂度 sqrt(n) )
/// huaszzh
#include
using namespace std;
typedef long long ll;
int main() {
ll n;
cin>>n;
ll ans=0;
for (ll i=1; ; i++) {
string s=to_string(i)+to_string(i); //将数字合并
ll t=stoll(s); //转成ll
if (t>n) {
cout<<i-1<<"\n";
return 0;
}
}
return 0;
}
思路2(huas_wlq):
我们注意到,对于任何一个“对称的数”都可以找到一个 x 使得 xx 是等于这个数的且对于所有的 x 符合上述要求,那么一定会有 x - 1 也符合上述要求(在 x - 1 不小于 0 的时候)。
也就是说,假设我们的答案是 ans 那么对于区间 [1, ans] 之间的所有数都会符合上述要求,那么对于整个区间 [1, n] 它是 满足二分性质 的(对于某个 check 函数而言,区间两侧的真假性不相同),所以我们可以 二分 求解。(时间复杂度 log(n) )
#include
using namespace std;
#define ll long long
ll n;
int ck(int x) {
/// 求得 xx 是否小于等于 n
ll sum = x, m = x;
while(x) {
/// 循环的次数取决于 x 的位数
x /= 10;
sum *= 10;
}
sum += m;
return sum <= n;
}
int main() {
cin >> n;
int l = 1, r = 1e9;
while(l < r) {
/// 二分答案
int mid = r + l + 1 >> 1;
if(ck(mid)) l = mid;
else r = mid - 1;
}
cout << l;
return 0;
}
G(huas_wlq、huaszq):玩具工厂
思路(huas_wlq):
题目抽象:我们每次可以选取 01 串中连续的一段将其删去,然后将被删除段的左右点在连接在一起继续进行删除操作,最少删几次后可以吧整个串删空。
由于每次选取的是连续的一段区间,我们不妨将连续的区间都缩小为一个点,即我们可以得到一个标准的、没有连续元素的 01 串。对于没有连续元素的 01 串来说,我们如果要将其删空那么是一定不会去选取左右端点进行操作的(除非仅剩端点的存在),这是因为,当我们删除掉中间的某一个 0 或者 1 之后,其左右两边的数字一定会相同,从而就相当于同时删去了两个区间,而端点处的点显然就没有这个性质了。
其实我们还有一种思考方式,即对于一个标准的 01 串,我们只需要删除掉其中所有的 0 或者 1 的位置,最后剩下的一定是一个“纯色”的串,直接就可以删掉。所以就不需要上面的模拟了,只需要数出不同的 01 的段数将其除二加一即可(如果 1 的段数多我们就删零,反之则删 1 )。
代码:下面是后一种思路的代码,模拟的代码就不贴了(我不会告诉你是因为我没写才不贴的)
#include
using namespace std;
int main(){
int sum = 1; /// 初值为 1 (至少有一段)
string s;
cin >> s;
for(int i = 1; i < s.length(); i ++) {
/// 计算 01 串的段数
if(s[i] != s[i - 1]) sum ++;
}
cout << sum / 2 + 1;
return 0;
}
H(duoduo):组成数字
思路(duoduo、huas_wlq):
大家还记得上次周测的签到题吗?我们讨论一个区间在取 k 个数的条件下的最大值和最小值的时候发现,对于这个最值区间中的每一个数我们都可以用 k 个数来表示出来,所以我们可以枚举被选取的数的个数(考虑到题目中给出的范围,我们从 1 开始枚举最多枚举到 1e6 的时候就可以保证一定枚举过所有的可能了),然后考虑在这个条件下是否能组成指定数字,选取其中的最大和最小值作为答案即可。
#include
using namespace std;
int main(){
int l, r, w;
cin >> l >> r >> w;
int ans1 = 0, ans2 = 0; /// ans1 存的是最小值
for(int i = 1; i <= 1e6; i ++) {
if(l * i <= w && r * i >= w) {
/// 可以证明,这里的乘积不会爆 int
if(ans1 == 0) ans1 = i;
else ans2 = i;
}
}
if(ans1 == 0) cout << "unsolvable";
else if(ans2 == 0) cout << ans1 << ' ' << ans1;
else cout << ans1 << ' ' << ans2;
return 0;
}
I(huas_chainz):找三元组
思路(the_xin、huas_wlq):
其实对于 c 的要求是很低的(正整数),那么在不考虑 c 的存在的情况下,题目就变为了:求出所有的 a * b < n (为了给 c 留空间所以不取等号)。
然后一切都变得简单了起来,直接两层 for 循环枚举所有可能的 a、b 的值之后累加即可(时间复杂度 e * n (这里的 e 是自然对数),当然,第二层可以用数学公式优化成 O(1) 的时间复杂度(甚至直接一次全优化掉包括第一层也是可以的),这样整体时间复杂度就会是 n )。
#include
using namespace std;
int main(){
int n;
cin >> n;
int ans = 0;
for(int i = 1; i < n; i ++)
for(int j = 1; j < (n + i - 1) / i; j ++)
ans ++;
cout << ans;
return 0;
}