好题——思维题
前言
本文章将会持续更新,主要是一些个人觉得比较妙的题,主观性比较强(给自己记录用的),有讲错请补充。
带 !号的题是基础例题,带 * 号的是推荐首先完成的题(有一定启发性的)。
算法思维题单
下面的题目都是码量较小,不是很好想的题。
P2804 神秘数字
先看数据范围:
\(n^{2}\) 很好做,求一下前缀和,暴力枚举一下。
\(2e6\) 用 \(nlogn\) 可通过。
我们继续来分析题目,我们可以把每个数剪掉平均数 \(m\) 再做前缀和,可以知道只要大于零,就从一到这个数是满足条件。
又看一看也可以发现只要后面的数大于前面的数就满足条件,也就是正序对。
我们只要在一以前加上一个零,又用树状数组求正序对就可以了(仿照求逆序对)。
Orac and Medians
找性质题。
做题思路:
当序列中没有 \(k\) 时,一定不可能。
当\(n=2\)时,平均数是小的那个数,所以一定要有数大于等于 \(k\)。
当\(n=3\)时,平均数就等于排名第二的数,但一旦有两个数相同,平均数就等于相同的那个数。
我们结合第二第三个性质时,就发现只要有两个相邻的数都大于等于 \(k\) 时,通过不断的变换就可以达成。
这时候还差了一个判断条件,性质三中平均数等于排名第二的,只要这个排名第二的数大于等于 \(k\) ,也就是一个数的两边都大于等于 \(k\) ,就满足条件。
启示:拆分问题,先探寻小的特殊情况,如,\(n=2,n=3\) 。再通过找到的性质推广到一般情况。
有一个相似的题:一个长度为 \(n\) 的序列,每次选其中的一个子序列,如果有数的个数超过这个子序列的一半就把序列中每一个数改为这个数,问最后可以改为多少种数。
其实就是找连续的三个数中有出现两次的数的种类数。
\(1 ,1, 2\) 于 \(1 ,2, 1\) 两种。
XOR-gun
题目描述
给定一个长为 \(n\) 的不降序列,每次操作可以任选相邻的两个数,并将这两个数替换为两个数按位异或的结果,现在需要破坏序列的不降,求最少操作次数,无解输出 \(-1\),\(n\) 的取值范围为 \(2e5\)。
我们知道两个数异或起来一定不大与左移两个数位数最大值再减一,如
\(11\) 与 \(100\) 异或起来不大与 \(111\) ,所以此题可能与二进制位数有关。
所以按照位数分块,这时我们又发现,当一个块中的数大于等于 \(3\) 时,肯定可以完成,直接输出 \(2\) 。这是就发现每个块最多只有两个,最多 \(31\) 个块,只要求一个前缀 \(n^3\) 暴力都可以过。
启示:异或的题可能与二进制位数有关。
P8775 [蓝桥杯 2022 省 A] 青蛙过河
此题看着无法下手的是 \(x\) ,数据范围比较大,无法暴力求。想过倍增,但每跳一次就要改变,故舍去。
可以把一个青蛙跳 \(2 \times x\) 次,转换成 \(2 \times x\) 个青蛙跳一次。(个人思路缺陷)
直接求不好求,想到用二分把求值变成判断。
当判断的距离为 \(mid\) 时,枚举每一个长度为 \(mid\) 的区间,判断这个区间中的高度和是否超过 \(2*x\)。
相当于在 \(1\) 到 \(mid\) 中,高度和大于 \(2x\),\(2x\) 个青蛙都有位置站,而移到 \(2\) 到 \(mid+1\) 时,在 \(1\) 上的青蛙就会跳到 \(mid+1\) 上,如 \(mid+1\) 可以存放下从 1 来的青蛙的个数,就成立。是否能放下可以看作求 \(2\) 到 \(mid+1\) 是否有大于 \(2x\) 个。
P6608 [Code+#7] 神秘序列
开始可以跟着样例手算一下,发现从前往后扫一遍只要有 \(a_i=i\) 就要变为零,不然先改变后面满足条件的 \(a_j\) 后,前面的 \(a_i\) 就超过 \(i\) 无法使之变为零。
正着算比较好算,只要扫到一个 \(a_i=i\) ,\(a_i\) 就变为零,\(a_1 \cdots a_{i-1}\) 加一。
但难点是构造出这个序列,可以想到倒着推回去,从前往后扫,只要找到一个零就把 \(a_i\) 变为 \(i\),\(a_1 \cdots a_{i-1}\) 减一。
可以试着打表看一下:
1:1
2:0 2
3:1 2
4:0 1 3
5:1 1 3
6:0 0 2 4
7:1 0 2 4
8:0 2 2 4
9:1 2 2 4
10:0 1 1 3 5
11:1 1 1 3 5
12:0 0 0 2 4 6
13:1 0 0 2 4 6
14:0 2 0 2 4 6
15:1 2 0 2 4 6
16:0 1 3 2 4 6
17:1 1 3 2 4 6
18:0 0 2 1 3 5 7
19:1 0 2 1 3 5 7
20:0 2 2 1 3 5 7
发现并没有什么规律,之前想过用一些数据结构使这个暴力方法优化到 \(1e6\) 的级别,发现很困难,故舍去。
又根据上表把每次改变的数的下标写下来。
1 2 1 3 1 4 1 2 1 5 1 6 1 2 1 3 1 7 1 2
发现 \(1\) 是两次一循环,除去 \(1\) 后 \(2\) 是三次一循环,再出去 \(2\) 后 \(3\) 是四次一循环,依此类推。
可以得出以下式子:
\(s_i\) 表示下为 \(i\) 的循环次数,\(k_i\) 表示减去前 \(i-1\) 的次数和(也就是还剩下的操作次数) 。
简化一下式子就是:
$sum $ 指还剩下的操作次数,这里把 \(k\) 数组省去了。
然后用每个数的循环次数来推整个序列。
\(s_i \times i\) 是 \(i\) 理论上要使 \(i\) 变成零的次数,而减去后面的数的次数和(也就是 \(i\) 被后面的数加一的次数),是 \(a_i\) 的原始值。
设原始的序列为 \(b\),那可以写出递推式:
这样就可以求出序列了。
但操作的次数是 \(n+k\) ,不知道次数,所以很容易想到二分来判断一下。
这道题二分的范围不好求,参考这篇,\(n\) 大概是:\(1.7724566 \times \sqrt(k)\) 。
code
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N=1e7+10;
int n;
int a[N],b[N];
bool check(int mid)
{
int k=mid+n,i=1;
while(k)
{
k=k*i/(i+1);
i++;
}
i--;
if(i<=mid) return 1;
return 0;
}
signed main()
{
scanf("%lld",&n);
int x=sqrt(n)*1.772456;
int r=x+100;
int l=max(x-100,2ll);
while(l<r)
{
int mid=(l+r)>>1;
if(check(mid)) r=mid;
else l=mid+1;
}
// if(l==x+100) 之前想错了,这里应该没有无解情况
// {
// puts("Daydream!");
// return 0;
// }
n+=l;
int len=1;
while(n)
{
int now=n;
n=n*len/(len+1);
a[len]=now-n;
len++;
}
len--;
int sum=0;
for(int i=len;i>=1;i--)
{
b[i]=a[i]*i-sum;
sum+=a[i];
}
printf("%lld\n",len);
for(int i=1;i<=len;i++) printf("%lld ",b[i]);
return 0;
}
P1758 [NOI2009] 管道取珠
此题只要想到求 \(\sum a_i^2\) 转化成:把管道复制一个相同的,求两个管道相同的序列数就可以了。
后面一个简单的 DP 。注意边界问题。
P8955 「VUSC」Card Tricks
此题做法比较多,个人觉得最简洁的是线段数加扫描线的算法。
如果单纯按照题目给的时间顺序维护序列的异或值是行不通的,无法判断是否有数超过给的 \(P\) 。
但我们利用或运算的单调递增性,可以试着用线段树维护时间轴。线段树第 \(i\) 个位置放的是第 \(i\) 次操作 or 上的值,如果第 \(i\) 次操作与该数无关,就放上 \(0\) 。这里相当与扫描线在 \(l\) 上或上 \(b_i\) ,在 \(r+1\) 上或上 \(0\),\(b_i\) 指操作序列以 \(i\) 开头的序列数。然后求每一个数时,二分一下就可以了。
Double Happiness
考的就是一个叫费马平方和定理
模(除以)4余1的素数可表示为两自然数的平方和
证明参考:费马平方和定理的天书证明
注意特判 \(2\) 这种情况也是对的。
染色问题
注:这些题目是一类问题,所以放在一起讲了,难度从低到高。
问题1:
一个 \(n\) 行 \(m\) 列的网格,初始时每一个格子都是白色(用数字 \(0\) 表示)。有两种操作:
- 把一行涂成一种颜色 \(x\)。
- 把一列涂成一种颜色 \(x\)。
该行(或该列)格子原有的颜色都会被覆盖成新涂上的颜色。
求操作后每种颜色的数量。
\(n,m \in 2 \times 10^6\)。
如果暴力做 \(O(n\times m)\),显然不行。
因为该行(或该列)格子原有的颜色都会被覆盖成新涂上的颜色。只要倒序操作,涂过的行列就删除。
问题2:
有一个 \(1 \times n\) 个格子,有 \(m\) 次操作,每次把 \(l\) 到 \(r\) 涂成 \(x\),同上题一样,格子原有的颜色都会被覆盖成新涂上的颜色,输出操作后每个格子的颜色。
\(n \in 10^7\)。
线段树维护肯定是不行的,有两种方式:并查集维护,链表维护。
大概方法:维护一个单向链表,倒序枚举操作次数,每次操作时 \(l\) 到 \(r\) 的数连到 \(r\) 的 nxt,相当于删掉 \(l\) 到 \(r\)。
void change(int l,int r,int col)
{
int now=0;
for(int i=l;i<=r;i=nxt[i])
{
if(!vis[i]) vis[i]=col;
nxt[now]=nxt[r];
now=i;
}
}
例题:P2391 白雪皑皑
问题3:
直接上题目链接:P9715 「QFOI R1」头
其实就是上面的结合。
但此题中有两种涂色方式,有一种是:如果涂色时遇到已经被染色的格子,就不再进行染色,我们正序删就可以了。
所以把两种方式分开,先倒序处理 \(t=1\) 的情况,然后正序处理 \(t=0\) 的情况。
code
#include<bits/stdc++.h>
using namespace std;
const int N=2e6+10;
int n,m,k,q;
struct node
{
int op,l,r,c,t;
}a[N];
long long ans[N];
struct lin
{
int sum,vis[N],nxt[N];
void change(int l,int r)
{
int now=0;
for(int i=l;i<=r;i=nxt[i])
{
if(!vis[i]) sum++;
vis[i]=1;
nxt[now]=nxt[r];
now=i;
}
}
int ask(int l,int r)
{
int res=0;
for(int i=l;i<=r;i=nxt[i])
{
if(!vis[i]) res++;
}
return r-l+1-res;
}
}tx,ty;
void change(int id)
{
if(a[id].op==1)
{
ans[a[id].c]+=1ll*(a[id].r-a[id].l+1-tx.ask(a[id].l,a[id].r))*(m-ty.sum);
tx.change(a[id].l,a[id].r);
}
else
{
ans[a[id].c]+=1ll*(a[id].r-a[id].l+1-ty.ask(a[id].l,a[id].r))*(n-tx.sum);
ty.change(a[id].l,a[id].r);
}
}
int main()
{
scanf("%d%d%d%d",&n,&m,&k,&q);
for(int i=1;i<=q;i++) scanf("%d%d%d%d%d",&a[i].op,&a[i].l,&a[i].r,&a[i].c,&a[i].t);
for(int i=1;i<=n;i++) tx.nxt[i]=i+1;
for(int i=1;i<=m;i++) ty.nxt[i]=i+1;
for(int i=q;i>=1;i--)
if(a[i].t) change(i);
for(int i=1;i<=q;i++)
if(!a[i].t) change(i);
for(int i=1;i<=k;i++)
printf("%lld ",ans[i]);
return 0;
}
P1315 [NOIP2011 提高组] 观光公交
贪心思维题(也有费用流大佬)
先考虑 \(k=0\) 的情况。
先预处理出每个站点最晚来的乘客。然后对于每一个点判断是车等乘客还是乘客等车,来更新从每一个站出发的时间。
然后考虑 \(k>0\) 的情况。
我们知道,在一次加速只会对后面的一段有影响。
1.到下一个点还人需要等待车:以后的时间就不再影响了
2.到下一个点不需要人等待车:对以后的时间还会加速直到……出现情况1,或者到最后一个点。
对于 \(k\) 次加速,每一次都找人数最多的那一段,然后更新那一段即可。
时间复杂度:\(O(n \times m)\)。
P4375 [USACO18OPEN] Out of Sorts G
先来看一道简单一点的题:P4378 [USACO18OPEN] Out of Sorts S
冒泡排序就是每次把一个大数移到后面去。
如:
1 5 3 8 2
如果排好序就是:1 2 3 5 8
先归位 \(8\),然后是 \(5\),最后是 \(3\)。
对于 \(2\) 来说,每次把一个比它大的数移到它的后面。
对于每个数都是把一个比它大的数移到它的后面。
所以答案就是:所有数大于它的个数中的最大值。
可以用树状数组求解。
然后我们来看这道题。
题意就是正向一次反向一次冒泡排序。
可以先像上面的做法考虑:
对于每一个数,每一次正反两次可能出现:一个比它大的数移到后面,然后几个比它小的数移到前面。这样是无法计数的。
那我们考虑离散化,把 \(n\) 个数变成序列。
考虑一个位置 \(x\),正反两次只会出现:一个比它大的数移到后面,一个比它小的数移到前面。
最后就是要求:每个位置大于它的个数中的最大值。
离散化加树状数组可解决此题。