目录
一、题目 3142—平方差
这个题目,乍一看!送分题! 实际上真送分吗? 如送。
首先你看限时3s,然后数据L,R<=1e9,好好好,直接暴力肯定是不行的。
并且对于一个x=y^2-z^2,y和z的大小可以超过x。为了避免暴力得到的答案集是完备的,看看最大的y能是多少,令y^2-(y-1)^2=R,即2y-1=R,得到y=(R+1)/2。1e9的数据,暴力不行了。
实在找不出来规律拿个40分跑路。。
既然知道是找规律了,那就找吧:
x=y^2-z^2=(y+z)(y-z),令a=y+z,b=y-z,x=a*b
得到
a+b=2y
a-b=2z
我们可以看出只有两种情况:(一奇一偶 和 为 奇)
①a和b都是奇数
如果a和b都是奇数,那么x也必然是奇数,没有偶数因子。
证明奇数一定可以找到一个y和z
令x=2k+1=(k+1)^2-k^2,则y=k+1,z=k
(同时我们发现0 1 4 9 16 25 36···· 的差分别为 1 3 5 7 9 11···奇数!)
②a和b都是偶数
如果a和b都是偶数,那么必然有x=a*b是4的倍数。
我们证明,4的倍数一定可以找到一个y和z。
令a=2k1,b=2k2 (a>=b,k>=0)
则a+b=2(k1+k2) y=k1+k2
a-b=2(k1-k2) z=k1-k2 得到。证毕
#include<bits/stdc++.h> using namespace std; int main() { int L,R; ios_base::sync_with_stdio(0); cin.tie(0); cin>>L>>R; int num=0; num+=(R-L+1)/2+((R-L+1)%2?R%2:0);//奇数的个数 num+=(R/4*4-(L+3)/4*4)/4+1;//4的倍数的个数 cout<<num; return 0; }
其中:R-L+1表示数字的个数。
R/4*4表示最后一个能被4整除的,(L+3)/4*4表示第一个能被4整除的。
如果这样的数字规律得不到,并且时间不够了,可以用最快速的循环先把答案算出来,虽然可能不能全部AC,但是可以多过很多点!别浪费了。
时间复杂度:O(1)
空间复杂度:O(1)
找规律还可以尝试一下这样:
将输出的内容放在Excel上排序,然后肉眼看看,看能不能发现点啥。。反正得相信,一定是有规律的。从前往后可以发现奇数都在,然后就是隔4来一个。当然再结合数学公式是最好的。
二、题目 3143—更小的数
这个题目,乍一看!诶?能有特殊方法吗?除了感觉是能用DP外,不晓得还有啥了,不过还没进行仔细思考。先看看直接暴力我遇到的问题吧。
暴力的时间复杂度>O(n^2),很难评。暴力是下下策
但是DP的话,可以用空间换时间达到O(n^2)必然可以过。
暴力
前两个和第三个的基本思路是一样的,但是第三个一个平台AC了,一个平台AC:96%
而前两个第一个平台只能AC:45%。问题出现在哪?
前两个的问题在于,你体现提前将子串弄好,再比较大小,这样的问题在于,你一定会遍历整个子串,不存在"剪枝"的情况,而第三种直接在原串的基础上比较,遇到成功就退出,遇到失败就退出,并不需要遍历所有子串,有的时候可以直接退出。前两种写法无论如何都需要遍历所有。因此即使是写起来简便,也不代表速度快!即使是暴力也要运行速度最快的暴力,而不是写起来最方便的暴力!
substr()这种东西还是少用,除非确实是必须得搞一个子串出来!reverse()也少用,除非必须翻转。比如能用下标解决的问题就别用这俩,下标解决的问题没有中间差价,substr()和reverse()有。
for(int i=0;i<n;++i){ for(int j=i+1;j<s.size();++j){ string t=s.substr(i,j-i+1); string m=t; reverse(m.begin(),m.end()); if(m<t) ++ans; } }
for(int i=0;i<n;++i){ for(int j=i+1;j<s.size();++j){ int k1=i; int k2=j; string t; for(int k=i;k<=j;++k){ t.push_back(s[k]); }//reverse(s.begin(),s.end()) /*比较*/ } }
#include<bits/stdc++.h> using namespace std; int main() { string s; ios_base::sync_with_stdio(0); cin.tie(0); cin>>s; int n=s.size()-1; int ans=0; for(int i=0;i<n;++i){ for(int j=i+1;j<s.size();++j){ int k1=i; int k2=j; while(k1<=j){ if(s[k2]<s[k1]){ ++ans; break; }else{ if(s[k2]>s[k1]) break; ++k1,--k2; } } } } cout<<ans; return 0; }
DP
全部AC。DP还是很容易想到可能可以用的,但是状态转移这块不是很快就能一下得到,考试的时候可以花点时间想想但是花太多还没有想到那只能跑路用暴力。
#include<bits/stdc++.h> using namespace std; int dp[5000];//表示前i个有多少种方案 bool Dp[5000][5000];//dp[i][j]表示 i~j是否可行 (用于判断相等时的情况) int main() { string s; ios_base::sync_with_stdio(0); cin.tie(0); cin>>s; int n=s.size()-1; for(int i=0;i<n;++i){//初始化长度为1,2的,长度为1是全局变量赋值为false。 if(Dp[i][i+1]=(s[i]>s[i+1])){//长度为2的在这里就判断完 dp[i+1]++; } } int ans=0; ans+=dp[1]; for(int i=2;i<s.size();++i){ for(int j=0;j<i-1;++j){//只判断长度≥3的 if(s[i]<s[j]){//肯定可行 dp[i]++; Dp[j][i]=true; }else{ if(s[i]==s[j]){//当两端值相等,则是否可行看中间是否可行 if(Dp[j+1][i-1]){ Dp[j][i]=true; dp[i]++; }else Dp[j][i]=false; }else Dp[j][i]=false; } } ans+=dp[i]; } cout<<ans; return 0; }
当然这里的dp[i]数组是非必要的,因为从前往后遍历,各个dp[i]是互不干涉的。
三、题目 3144—颜色平衡树
看到这个题有两种想法:
①从叶子结点开始向上遍历,即从输入的数组中从后往前遍历即可,每次遍历告诉其父节点它的颜色和数量,并判断自己这个结点的子树中是否都可以成功。
②构造树,用后根遍历,后根遍历保证了,和①一样的结构,但是不同的是这里用递归操作,使得每一层的map使用完之后就撤销了,不会导致内存超限。<dotcpp AC了>
上面一种情况的Map开得太多了。
第一种方法每个结点维护自己的子树颜色种类和数量,并全部告诉父亲,AC:91%
问题在于,每次都要告诉其父亲,每个结点都要判断一次颜色值是否相同,时间复杂度:
O(颜色数量*结点数量*2),问题好像出现在Map一次性开得太多了。 //unordered_map查找平均是O(1)的。
#include<bits/stdc++.h> using namespace std; int main() { int n; scanf("%d",&n); int Node[200001][2]; unordered_map<int,int> Map[200001]; for(int i=1;i<=n;++i){ scanf("%d %d",&Node[i][0],&Node[i][1]); } long long ans=0; bool flag; for(int j=n;j>=1;--j){ Map[j][Node[j][0]]++; int fa=Node[j][1]; int num=Map[j].begin()->second; flag=true; if(fa!=0){ for(auto it=Map[j].begin();it!=Map[j].end();++it){ Map[fa][it->first]+=it->second; } } for(auto it=Map[j].begin();it!=Map[j].end();++it){ if(num!=it->second){ flag=false; break; } } if(flag) ++ans; } printf("%lld",ans); return 0; }
第二种写法本质上是一样的,只是Map数组的大小变了!
不过需要理解dfs,后根遍历。
dsf:传入根结点及其父亲的Map,对于每一个结点,后根遍历结束之后,它的map中就存有了该子树的所有颜色及其值。
然后将该颜色和值再传给它的父亲的Map中。
#include<bits/stdc++.h> using namespace std; int n; int color[200001]; long long ans; vector<int> childs[200001]; void dfs(int root,unordered_map<int,int> &Map){//为其父亲送上自己的大小,并且判断自己的子树是否平衡 if(childs[root].size()==0){//叶节点 Map[color[root]]++; ++ans; return; } unordered_map<int,int> mp; mp[color[root]]++; for(int i=0;i<childs[root].size();++i){ dfs(childs[root][i],mp); } bool flag=true; if(root!=1) //root=1传给0号结点无意义 for(auto it=mp.begin();it!=mp.end();++it){ Map[it->first]+=it->second; } auto it=mp.begin(); int num=it->second; for(++it;it!=mp.end();++it){ if(num!=it->second){ flag=false; break; } } if(flag) ++ans; return; } int main() { int n; scanf("%d",&n); for(int i=1;i<=n;++i){ int fa; scanf("%d %d",&color[i],&fa); childs[fa].push_back(i); } unordered_map<int,int> Map; dfs(1,Map); printf("%lld",ans); return 0; }
树链剖分+树上启发式合并
四、题目 3145—买瓜
采用搜索+后缀和剪枝:
把质量按从大到小排序,然后计算后缀和,当后面质量+前面遍历质量小于m则直接跳出本次搜索,因为即使是加上了后面的所有瓜,都不能满足要求。我们不采用从小到大是因为,从小到大没有这样的后缀性质,当最大的已经大于m时,很显然后缀和的效果基本上就不存在了,剪枝效果不行
当然除了这种技巧之外,剪枝还有很多!比如:当前分片已经大等于了记录下来的最小分片值,那就不用再遍历了!因为再遍历也只能分片数量变多呀!~
#include<bits/stdc++.h> using namespace std; long long sum[31]; int output=-1; long long m,n; long long mellon[31]; void dfs(long long cur_sum,int cur_mellon,int half_num){//当前总重量cur_sum,当前遍历的西瓜编号cur_mellon if(cur_mellon==n) return;//遍历结束了 if(cur_sum+sum[cur_mellon]<m) return; if(half_num>=output&&output!=-1) return; for(int i=cur_mellon;i<n;++i){ if(cur_sum+mellon[i]==m){ if(output==-1||output>half_num) output=half_num; }else if(cur_sum+mellon[i]<m) dfs(cur_sum+mellon[i],i+1,half_num); if(cur_sum+mellon[i]/2==m){ if(output==-1||output>half_num+1) output=half_num+1; }else if(cur_sum+mellon[i]/2<m) dfs(cur_sum+mellon[i]/2,i+1,half_num+1); } return; } bool cmp(int a,int b){ return a>b;//从大到小排序 } int main() { cin>>n>>m; m=m*2;//为了让分半变整,整体翻倍~! for(int i=0;i<n;++i){ cin>>mellon[i]; mellon[i]*=2; } sort(mellon,mellon+30,cmp); for(int i=n-1;i>=0;--i){ sum[i]=sum[i+1]+mellon[i]; } dfs(0,0,0); cout<<output; return 0; }
五、题目 3146—网络稳定性
本题一看就知道很难,考虑暴力跑了。这里拿到30分可以用Floyd算法的思想。
(本题的路径也是简单路径,因为如果能重复走的话,就变成了连通图(并查集)中边权最小的边了)
回忆一下Floyd,Floyd算法本质上是一个动态
规划:
dp[i][j]表示的是i到j的最短路,如何进行状态转移?
我们考虑不断加入中间结点。
刚开始对相邻结点的最短路进行初始化。
然后依次加入结点,考虑状态转移:
dp[i][j]=min(dp[i][j],dp[i][k]+dp[k][j]) //i到j的最短路 可以分解为i到k的最短路 然后k到j的最短路。不断加入结点k,加入一个更新所有结点。
相当于状态等价于dp[k][i][j]:能以前k个结点作为中间结点时,i到j的最短路。
因此dp[k][i][j]=min(dp[k-1][i][j],dp[k][i][k]+dp[k][k][j]) 二维是简略写法。
本题Floyd:
dp[k][i][j]表示能以前k个结点作为中间结点时,i到j的最高稳定性。
则dp[k][i][j]=max(dp[k-1][i][j],min(dp[k][i][k],dp[k][k][j]))
因为,一条路径上的稳定性取决于路径上边权最小的边,因此经过k点的路径的稳定性是两条路径的稳定性取最小值,但是总体取最大是因为 两点的稳定性与拥有最高稳定性的路径有关。
简写:dp[i][j]=max(dp[i][j],min(dp[i][k],dp[k][j]))
#include<bits/stdc++.h> using namespace std; int dp[5002][5002]; int main(void){ ios_base::sync_with_stdio(0);cin.tie(0); int n,m,q; cin>>n>>m>>q; while(m--){ int u,v,w; cin>>u>>v>>w; dp[u][v]=dp[v][u]=max(dp[u][v],w); } for(int k=1;k<=n;++k) for(int i=1;i<=n;++i) for(int j=1;j<=n;++j){ if(i!=j&&dp[i][k]&&dp[k][j]){ dp[i][j]=max(dp[i][j],min(dp[i][k],dp[k][j])); } } while(q--){ int x,y; cin>>x>>y; if(dp[x][y]==0) dp[x][y]=-1; cout<<dp[x][y]<<'\n'; } return 0; }
六、题目 3147—异或和之和
看题目O(n^2)只能过60%,但是能过60%先过60%再说。前缀和很容易想到。
在这里本来想用一个东西(a+b)^c=a^c+b^c 这样就可以O(n)解决,但是这是不成立的!!!
比如(1+2)^3=0 1^3+2^3=3 所以这个写法不行(数论知识要用的时候一定要验证一下正确性)
#include<bits/stdc++.h> using namespace std; int main(void){ ios_base::sync_with_stdio(0);cin.tie(0); int sum[100002]; int arry[100002]; int n; cin>>n; long long ans=0; sum[0]=0; for(int i=1;i<=n;++i){ cin>>arry[i]; sum[i]=sum[i-1]^arry[i]; } for(int i=0;i<=n;++i){ for(int j=i+1;j<=n;++j){ ans+=sum[j]^sum[i]; } } cout<<ans; return 0; }
O(nlogn)
异或运算和加法运算不同,异或运算没有进位,即两个数异或的结果只跟每一位的一和零有关,在我们求出sum[i]之后,要求出最终结果,本质上就是求任意两个sum[i]的异或 之和,即对任意两个数都求一次异或,然后求其和。我们是否能够通过二进制位来查看异或的特征?
对于不同二进制位,我们知道权值是不一样的,对于某一位而言,它对答案的贡献如何看?
我们说异或,不同则为1,不同即0和1则为不同,对于某一位,比如:
有a=0,b=1,c=1,d=1,e=0,我们可以发现它们两两异或之和的结果 之和1和0的组合有关,即这里两个0、三个1,则一共2*3=6种异或结果为1的情况。
我们把这种结果推广到sum[i]的每一位。
我们对每一位进行推断,发现对于某一位可以产生m个1,n*(n+1)/2-m个0,在做加法时,0不会对结果产生影响,而1产生的影响与该位的权值有关,比如该位对应的二进制权值是2^k,则所有sum[i]该位对结果的贡献是2^k*m。
因此我们只需要求出每一位的0和1的个数就可以得到答案。
需要注意的是:
(1)sum[0]也需要考虑在内,不然无法得到第1个数到第i个数的异或值。
(2)计算0和1的数量与最大数字的位数有关,如果某个数的值是0,它同样也要记录那么多个位数的0的个数!
#include<bits/stdc++.h> using namespace std; int result[21][2]; int main(void){ ios_base::sync_with_stdio(0);cin.tie(0); int sum[100002]; int n; cin>>n; long long ans=0; sum[0]=0; for(int i=1;i<=n;++i){ int a; cin>>a; sum[i]=sum[i-1]^a; } for(int i=0;i<=n;++i){//求sum[i]中某一位的0和1 int pos=0; while(pos<21){ result[pos][sum[i]&1]++; ++pos; sum[i]>>=1; } } int num=1; for(int i=0;i<21;++i){ ans+=long(num)*long(result[i][0])*long(result[i][1]); num*=2; } cout<<ans; return 0; }
十、问题总结
(1)for循环容易自动写一个++i,但实际上有的时候i的值在for循环里面更改,因此不需要++i,如果写的时候没注意很难发现。
这种情况要养成写while循环的习惯。
int num=0; for(int i=0;i<access.size();){//不需要++i if(greedy[i]==i){//最远的居然是当前位置?说明没有符合要求的 cout<<-1; return 0; }else{ i=greedy[i]; if(i<access.size()) ++num; } } cout<<num;
(2)substr()和reverse()非必要的时候少用,因为它们一定会带来一次遍历。而对于一些问题,我们可能并不需要一次完整的遍历,因此我们用它们就会导致更多的耗时。