ACM散题习题库 1【持续更新】
收录了部分有趣的题目:
【一句话,干就是了!】
目前学习进度:图论→匹配问题→背包/区间/状压DP→数论
字符串:
题目一 : 2008年老题,但是网上没有题解【 vjudge 可以看 sourse code 】,最后被bs学长搞了。(第一次接触01BFS。
暴力枚举:
题目二 :
①思路问题:因为最大值的变化性,一直写不出来【应该反思自己写代码的能力了】,后来看了题解,发现对于最大值选择与哪一个值匹配,我们是可以枚举的【TLE怕了,不敢写,不敢想】,但是n=1000是完全可以写的,而且题目还保证了:
【It is guaranteed that the total sum of nn over all test cases doesn't exceed 1000】
②码力问题:我看到别人用multiset模拟时,我也试着写了一遍,后来发现一直异常。。【原因出在了:s.erase(s.end())】
然后,也有很多人使用map来记录每一个数的cnt,然后 模拟multiset【我不会啦。。】
③总结:学习到 #define iter(c) __typeof((c).begin()) 、 函数传参传vector等stl容器时,是按值传递的,不是传地址,
也就是说,修改传进来的vector不会改变原来的vector
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #define re register #define ll long long #define fi first #define se second #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define max(a,b) ((a)>(b)?(a):(b)) #define min(a,b) ((a)<(b)?(a):(b)) #define me(a,b) memset(a,(b),sizeof a) using namespace std; const int maxn = 1e5+11; int n,b; vector<int>v; vector<pair<int,int> >vp; void solve() { multiset<int>s; v.clear(); bool flag = 1; cin>>n;n*=2; for(int i = 0;i < n;i++) cin>>b,v.emplace_back(b); sort(v.begin(),v.end()); for(int i = 0;flag&&i<v.size()-1;i++) { s.clear();vp.clear(); for(int j = 0;j < v.size()-1;j++) if(j!=i)s.insert(v[j]); int pre = v[n-1]; while(s.size()) { auto it = s.end(); it--;//必须--,因为s.end()是指向外面的外界 int bk = *it; int want = pre - bk; s.erase(it);//每erase一次,s.end()向前移一位 //s.erase(s.end()); if(s.count(want)) { s.erase(s.find(want)); vp.emplace_back(make_pair(bk,want)); pre = max(bk,want); }else break; } if(s.empty()) { cout<<"YES"<<endl; cout<<v[n-1]+v[i]<<endl; cout<<v[n-1]<<" "<<v[i]<<endl; for(int i = 0;i < vp.size();i++) cout<<vp[i].fi<<" "<<vp[i].se<<endl; flag = 0; break; } } if(flag) cout<<"NO"<<endl; } int main() { /*multiset<int>s; for(int i = 1;i <= 10;i++) s.insert(i); for(auto it=s.begin();it!=s.end();it++) cout<<*it<<" "; cout<<*s.end()<<endl;//10 s.end() 指向的元素值等于最后一个元素 //但是erase千万不能s.erase(s.end()) 因为s.end()并不在s的范围之内 auto it = s.end(); cout<<" s.end() = "<<*it<<endl;//10 --it; cout<<" s.end() = "<<*it<<endl;//10 --it; cout<<" s.end() = "<<*it<<endl;//9 --it; cout<<" s.end() = "<<*it<<endl;//8*/ int t;ios; cin>>t; while(t--) solve(); }
题目三 :前后缀+构造+思维题目 ,【考的时候确实不会,说起来容易做起来难(哭)】, 首先要会普通的抵消石子的游戏,然后再一个一个点进行枚举看看更换之后是否能够成功(最朴素的做法)。加入前后缀记录优化,可以大幅降低运行时间。
首先来看最普通的抵消石子:我们知道最后 能成功抵消的就只有一种情况,就是 a1 < a2 > a3 这样的局面,此时
若 a1 + a3 == a2 那么就是YES 。 所以我们可以开一个 suffix数组,一个 prefix数组,分别记录如果不需要转移的时候抵消到 i 所剩下的石子。
① s[ i ]表示从最后面(第n个)开始抵消到 i 时,i还剩下的石子数量
② p[ i ]表示从第一个开始抵消到 i 时还剩下的石子数量
③ 如果存在 p[ i ] == s[ i + 1 ] ,那么就说明不需要转移(superpower)就可以完成。 (可以模拟一下)
再来看加上superpower的情况:因为我们转移只是影响到 第i位 和 第i+1位,其它石子堆的抵消方式并不会产生改变,我们就特判一下就好了【不要忘了判断 a 与 s,p 大小时加上等号,比如数据 : 2 2 1 8 9 7 7 】
题目四 : 这道题也是前后缀,但是用线段树维护最大最小值也能A(尬) 其实两者的思路是一样的,把字符串化为直角坐标系的一条曲线,然后断开询问区间(L,R)之后,将 R 右边的曲线并到 L 左边的断口处 ,所以我们需要维护前区间最大最小值,然后维护后区间最大最小值,然后后区间最大最小值还要加上 L ,R 两个断口的在y轴上的差距。
维护区间最值显然是线段树的特长,但是使用(后缀+前缀和)来维护最值我还是第一次见。。
CF题解各变量的含义:sur后缀区间相对最大值,sul后缀区间相对最小值,pr前缀和,prr前缀最大值,prl前缀最小值
题目五 :D. Yet Another Subarray Problem 动态规划,定义dp[ i ][ j ]为以 i 结尾时,i 是所选区间的第 j 个数,如果是第一个数就要减 k ,如果不是就考虑继承。
题目六 :思维题 / 前后缀
解法一: 我们维护①左、右两个指针(x,y),再②维护指针上下左右所能到达的最远距离(maxl,maxr,maxup,maxdown),最后再③维护指针到上下左右界的最远距离【bestl,bestr,bestdown,bestup】(就是指针在移动过程中所能达到离maxl,maxr,maxdown,maxup的最远距离,这个最远距离的最大一个就是矩阵的宽 和 高)。
bestl = max( abs(maxl - x) ) bestr = max(abs(maxr - x))
bestup = max( abs(maxup - y )) bestdown = max(abs(maxdown - y ))
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #define re register #define ll long long #define fi first #define se second #define debug(a) cout<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} using namespace std; ll maxl,maxr,maxup,maxdown; ll bestr,bestl,bestup,bestdown; string s; int main() { ios; int t; cin>>t; while(t--) { cin>>s; ll len = s.size(); maxl = maxr = maxup = maxdown = 0; bestl = bestr = bestup = bestdown = 0; ll x = 0,y = 0,x1 = 0,y1 = 0; for(int i =0;i <len;i++) { if(s[i]=='W')y++; else if(s[i]=='S')y--; if(s[i]=='A')x--; else if(s[i]=='D')x++; maxr = max(maxr,x); maxl = min(maxl,x); maxup = max(maxup,y); maxdown = min(maxdown,y); bestl = max(abs(x-maxl),bestl); bestr = max(abs(x-maxr),bestr); bestup = max(abs(y-maxup),bestup); bestdown = max(abs(y-maxdown),bestdown); } ll n = maxup-maxdown+1,m = maxr-maxl+1; //debug(n),debug(m); if(bestl!=bestr&&bestup!=bestdown&&n>2&&m>2) cout<<((m>n)?(n-1)*m:(m-1)*n)<<endl; else if(bestl!=bestr&&m>2) cout<<(m-1)*n<<endl; else if(bestup!=bestdown&&n>2) cout<<(n-1)*m<<endl; else cout<<n*m<<endl; } }
解法二(前后缀思路):【一位神牛的讲解(转)】
这个字符不是随便可以加的,因为会出现,你加了一个字符
使左边的最小值往右边移了一下,但是导致最右边往右移了,得不偿失
首先我们考虑垂直方向,我们假设它在0这个位置,那么他所移动的
位置就是差值为1的点,我们在一个位置前面加了W,那么他后面所有点的坐标都加一
如果你在一个位置前加了s,那么他后面的所有点的坐标会减一,
你要保证可以减小,那么你加一的时候,后面不能有最大值,并且前面不能有最小值
减一的时候后面的不能有最小值,并且前面不能有最大值,否则都不可以。
所以要找那种,可以加的情况,就是加了可以减少,不会增加也不会不变的那种
因为垂直和水平情况一样,我们只考虑一边
我们必须明确你要加一,那么后面的全部加一,你要减一那么后面的全部减一
垂直
假如它达到了很多次最大值和很多次最小值
1、当最后一个最大值在第一个最小值前面的时候,我们只要将后面的值加一,最小值减少
而你改其他的位置,要么会使值变大,要么会使值不变
2、当最后一次最小值出现在第一个最大值之前时,只要将最小值后面的减一,那么值也会变小
其他两种情况改了和没改一样,或者造成变大
题目七 : D. Extreme Subtraction 思维 / 差分 / 贪心(易理解的贪心):
这里讲一下差分的做法:【记住,差分数组一般用于区间加减操作】
原数组: a0 a1 a2 a3 ................ an
差分数组:d0 d1 d2 d3 .................dn dn+1【多一项】
其中 d0 = a0 , d1 = a1 - a0 , d2 = a2 - a1 ,d3 = a3 - a2 ............................ , dn+1 = - an
根据差分数组的性质,在差分数组两个端点加减既可以完成在原数组加减的任务
那么我们就只需要将 d中小于0的加一,同时 d1 - 1, d中大于0的减一 ,同时dn+1 + 1
如 1 10 9 10 10 ---> 差分数组 : 1 9 -1 1 0 -10 【分别从 d1 到 dn+1】 1 和 -1 抵消,-10 和 9+1抵消,所以这个数列是YES的
题目又说左右两个端点【即a1,an可以自减,所以就不用管了】,我们只需要将 d(2~n)化为0,然后判断以下左右两个端点是否大于等于0就可以了。【综上,这道题不是差分板子题我直播吃掉电脑屏幕】
#include <bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=3e4+7; int n,m,p; int a[maxn],b[maxn]; int main(){ int t,T=0; cin>>t; while(t--) { ll sum=0; scanf("%lld",&n); for(int i=1;i<=n;i++) { scanf("%lld",&a[i]); b[i]=a[i]-a[i-1]; } for(int i=2;i<=n;i++) { if(b[i]<0) sum-=b[i]; } if(a[1]>=sum) cout<<"YES"<<endl; else cout<<"NO"<<endl; } return 0; }
#include<bits/stdc++.h> using namespace std; #define x first #define y second typedef long long ll; int qsm(int a,int b,int c) { int res=1; while(b) { if(b&1) res=res*a%c; b>>=1; a=a*a%c; } return res; } const int N=30010; int a[N]; int d[N]; int n; bool isok() { int res=0; for(int i=2;i<=n;i++) if(d[i]>0)res+=d[i]; return d[n+1]+res<=0; } int main() { int t; cin>>t; while(t--) { cin>>n; for(int i=1;i<=n;i++) cin>>a[i],d[i]=a[i]-a[i-1]; d[n+1] = -a[n]; if(isok()) cout<<"YES"<<endl; else cout<<"NO"<<endl; } }
题目八 : 【逆康托展开】数学题,快速求出 n 个数的第 k 个全排列!!!
【摆脱循环k次next_permutation就在现在!!】
第一步:我们需要一个数组fac预处理出1~n的阶乘。
第二步:调用kth_permutation函数.....这么简单??对啊,诶,你别走啊,我板子还没给你。
先来看看原理:
①n个数的序列一共有n!种排列方法
②我们先确定排在第一位的数字,那么n个数的序列,就降级为n-1个数的序列
③循环第二步,直到所有数按位排排坐好。
模拟过程:【更详细请百度 康托编码 / 全排列生成模板】
1 2 3 4 5 的第64个排列:
①由定理一(因为后四位数字全排列有24种)知第1~24个排列时,1排在第一位,第25~48,2排在第一位,第49~72,3排在第一位【后面以此类推+24】
②64处于49~72之间,所以我们取出3,把他放在答案序列第一位。
③现在剩下4个数字,所以问题转换为求这四个数的第(64-48=16)个排列。
④(后三位数的全排列有6种),1~6是1在第一位,7~12是2在第一位,13~18是3在第一位【由于3取了出来,4代替3】
⑤现在答案序列就是 3 4 ,剩下1 2 5没归队,我们要求第(16-12=4)个排列。
⑥(后两位数全排列有2种)1~2是1第一位 ,3~4是2第一位,所以取2,答案序列为 3 4 2
⑦(剩下两个数 全排列为 1 5 和 5 1)我们要选第(6-4=2)个,即 5 1
⑧所以答案是 3 4 2 5 1
//n个数,第k个排列,下标从0开始 //k--重要一步(不知道,反正没了不行 int fac[25]; vector<int> kth_permutation(int n,long long k) { /*建议放在函数外面预处理 fac[0] = 1; for(int i = 1;i <= n;i++) fac[i] = fac[i-1]*i; */ vector<int>v,ans; for(int i = 1;i <= n;i++)v.push_back(i); if(k==0)return v; k--; for(int i = n;i > 0;i--) { int base = fac[i-1]; int tok = k/base; k %= base; ans.push_back(v[tok]); v.erase(v.begin()+tok); } return ans; }
HDU1496 : Hash练习题。
判断等号两边是否相等。我们将等式化为 x1 x2 在左 ,x3 x4在右,然后枚举n^2,并且使用两个数组记录以下数值所到达的地方【记住,能到达的值是+1,而不是=1】,然后我们在枚举的过程中统计答案就好。
注意特判:由于数据原因,特判减去很多时间【但是不管数据 怎么样,正负两方的特判都是我们理所当然应当想到的】
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #pragma GCC optimize(3) #define re register #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cout<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} const int maxn = 1e6+1111; int pos[maxn],neg[maxn]; int n,m,a,b,c,d; int ans = 0; int main() { while(~scanf("%d%d%d%d",&a,&b,&c,&d)) { //特判正负的情况,直接从2s降到200ms if(a>0&&b>0&&c>0&&d>0||a<0&&b<0&&c<0&&d<0)puts("0"); else { me(neg,0),me(pos,0);ans = 0; for(int i = 1;i <= 100;i++) for(int j = 1;j <= 100;j++) { int x = i*i*a+j*j*b; if(x>=0)pos[x]++;//别忘了等于0的情况 else neg[-x]++; } for(int i = 1;i <= 100;i++) for(int j = 1;j <= 100;j++) { int x = i*i*c+j*j*d; if(x>0)ans+=neg[x]; else ans+=pos[-x]; } printf("%d\n",ans*16); } } }
Nowcoder11038/D :一道本来 很简单但是被我想得很复杂的题目 。题目要求【在走最少的路的前提下,那么走最少,也就是各个节点只需要联通就 可以了,所以直接最小生成树。。。】我一直在想,如果没有了这个最少的路的条件,题目 应该怎么做呢?
Nowcoder11038/C:难题【思维+容斥+组合】
我想到了容斥,但是我不是从小到大来计算的【所以挂了】。之所以从小到大,是因为大的不会对小的产生影响,而小的影响大的。
我们先将a数组小到大排序,考虑怎么计算:
对于第i只猫咪,它的名字还剩下 \( 26^{1} + 26^{2} + ... + 26^{a_{i}} - (i - 1) \) 种选择,所以直接乘进去答案即可
关于这个程序有一个明显的bug,因为sum数组是取模的,不能直接和 \( i - 1 \)判大小,而是应该定义一个struct表示是否已经大于mod才行
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #pragma GCC optimize(3) #define re register #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cout<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} const int maxn = 1111,mod = 77797; const double eps = 1e-4; ll p[111],sum[111],a[maxn],n,m,ans; int main() { p[0] = 1; for(int i = 1;i <= 11;i++)p[i]=p[i-1]*26%mod; for(int i = 1;i <= 11;i++)sum[i]=(sum[i-1]+p[i])%mod; while(~scanf("%lld",&n)) { bool f = 1; for(int i = 1;i <= n;i++) scanf("%lld",&a[i]); ans = 1; sort(a+1,a+1+n); for(int i = 1;i <= n;i++) { if(sum[a[i]]-i+1<=0) { cout<<-1<<endl; f = 0;break; } ans = (ans*(sum[a[i]]-i+1))%mod; } if(f)cout<<ans<<endl; } }
题目十二:【二分判断答案】给你一个长为n的数组,两个整数n,m,将这个数组分为连续的m份,请你选一种分法,使得这m份之中(所有数的和)的最大值最小。
这道题我们可以预处理出数组的【所有数的和】和【数组最大的数】,然后 二分范围就是【maxx~sum】
我们选一个数 mid 作为最大的【sum】中的最小值,然后判断能否分成m份,如果不能,就让 mid 小一点,否则,就让 mid 大一点。最后输出 mid 。
总结:一般难以想到解决方法的题目,我们可以试着判断答案是否具有单调性【这句话:如果不能,就让 mid 小一点,否则,就让 mid 大一点。如果问题存在一个界限,那么我们就试一下去二分答案。】平时多留一个心眼想想题目,多做多练。
CF div2 CONTEST 题目十三~十五:
第一题:
Hills and valleys : 【暴力模拟+枚举】一道非常简单的题目,竟然又无从下手 。。一个十分简单的思路,我们将 n 个值都修改一遍,然后跑一遍O(n) 寻找最小值答案,但是这样做法是 O(n方) 。 我知道每个值要么改到与左边一样,要么改到与右边一样,就能消掉这个 山谷/山峰 ,然后顺势左右两个 山谷/山峰 甚至也能消掉。所以预处理使用 is数组记录之前是不是山峰/山谷,之后直接改变 第i位的值 ,就再调用ishiil/isvalley函数判断一下,取差值为 d ,答案为 : ans = min(ans , ans + d ).
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #include <stack> #pragma GCC optimize(3) #define re register #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cout<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} inline ll read(){ ll x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int maxn = 3e5+11,inf = 0x3f3f3f3f3f3f,mod=1e9+7; int a[maxn],n,k,m,tot,ans,is[maxn]; int isone(int x) { if(x>1&&x<n&&a[x-1]<a[x]&&a[x]>a[x+1])return 1; if(x>1&&x<n&&a[x-1]>a[x]&&a[x]<a[x+1])return 1; return 0; } int main() { int t,x,v; t = read(); while(t--) { n = read(); ans = inf, tot = 0; me(is,0); for(int i = 1;i <= n;i++) a[i] = read(); for(int i = 1;i <= n;i++) if(isone(i))tot++,is[i]++; for(int i = 2;i < n;i++) { int temp = a[i]; a[i] = max(a[i-1],a[i+1]); ans = min(ans,tot-is[i]-is[i-1]-is[i+1]+ isone(i-1)+isone(i)+isone(i+1)); a[i] = min(a[i-1],a[i+1]); ans = min(ans,tot-is[i]-is[i-1]-is[i+1]+ isone(i-1)+isone(i)+isone(i+1)); a[i] = temp; } printf("%d\n",(ans==inf?0:ans)); } } //10 1 2 1 2 1 2 1 2 1 2
第二题:
Three bags: 【思维题】又是我没想到的题目
题解:
只进行奇数次操作的数是负数,进行偶数次是正数
我们将只进行一次操作的数称为【桥】,可以知道,【桥】的值最后是负数的。
第一种情况:我们把两个最小值当作【桥】【这两个最小值一定 要在不同背包】
做法:我们只需要取三个背包中选取每个背包中最小的数出来再对比一次,取出三个最小值中最小的两个,再求和 : mn = min1 + min2 + min3 - max(min1,max(min2,min3)) 。
第二种情况:我们取一个背包里面所有的数当作【桥】
做法:求出三个背包的 sum 值,取最小的那个。
这样做的原因是,如果一个背包的数全是1,有100个,但是剩下两个背包中只有一个数,都是1e9。那么我们就要减掉第一个背包中的所有数。
ans = sum1 + sum2 + sum3 - min( mn , minsum )*2; 乘二是因为sum1+sum2+sum3里面重复里,所以要多减一次
第三题:
sum of paths :动态规划。虽然很多人说简单。。。【屁】
第一步:我们先要用 DP 求出【走 i 步后停在 第 j 个点的 种数】
dp[ i ][ j ] = dp[ i-1 ][ j-1 ] + dp[ i-1 ][ j+1 ]
第二步:如果走 i 步后以 j 为终点,那么我们从 j 出发,也可以走 i 步回去,所以【走 i 步后停在 第 j 个点的 种数】等于 【 从第 j 个点出发 走 i 步的种数】
第三步:我们上面那样求,其实是默认第 i 个点是必走的,根据组合数原理,选一个起点和一个终点走k步【可重合】,就是 cnt[ i ] = dp[ k-j ][ i ] * dp[ j ][ i ] 。
之后就是 a[ i ]*cnt[ i ] 累加起来就是答案了。
每一次询问只是更新数值,并没有改变经过 该点 的次数,所以O(1)询问回答就可以了
总复杂度:O(n^2) + O(q)
CF div2 CONTEST 题目十六~十七:【CF ~ div2】
第一题:
BFS染色求最小点覆盖 :【好题!!】定义老师为【主节点】,【主节点】要覆盖所有的主要边,即最小生成树上的边,而且所有的主要点不能相邻。【这应该是一个最小点覆盖的问题吧。】
特别留意的是,这道题的染色不能使用类似 DFS 的染色法【即染一个点,就 访问一个点,这样无法保证【主节点】不相邻,就算发现了两个主节点相邻,也改不了 。】
#include <stdio.h> #include <iostream> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #include <stack> #pragma GCC optimize(3) #define re register #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cout<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} inline ll read(){ ll x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-') f=-1; ch=getchar(); } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } const int maxn = 3e5+11,inf = 0x3f3f3f3f3f3f,mod=1e9+7; vector<int>e[maxn],ans; int cnt,n,m,a,b,c,col[maxn]; bool vis[maxn]; bool bfs(int x) { //sum变量记录访问过的节点数量 ,以此判断一个图是不是n个节点 //cnt记录col为1的节点数量 int sum = 1; queue<int>q; q.push(x); cnt += col[x] = 1; vis[x] = 1; while(!q.empty()) { x = q.front(); q.pop(); for(int v:e[x]) { if(col[v]==-1) cnt += col[v] = col[x]^1; //加上一个col==1的节点,如果col==0,就cnt+=0 else if(col[v]==1&&col[x]==1) col[v] = 0,cnt--; //删掉一个col==1的节点,cnt-- } for(int v:e[x]) if(!vis[v])vis[v]=1,sum++,q.push(v); } return sum == n; } int main() { int t = 1,kase = 1; ~scanf("%d",&t); while(t--) { n = read(),m = read(); for(int i = 0;i <= n;i++) col[i]=-1,vis[i]=0,e[i].clear(); cnt = 0; for(int i = 1;i <= m;i++) { a=read(),b=read(); e[a].emp(b),e[b].emp(a); } if(bfs(1)) { printf("YES\n%d\n",cnt); for(int i = 1;i <= n;i++) if(col[i]==1)printf("%d ",i); puts(""); }else puts("NO"); } }
第二题:
hash灵活运用于记录特征值 :本题题目我都看懵了。。【直到后来发现,product:乘积。。。】
hash特征值在于如何设计一个hash函数使得具有相同特征值的 元素 的 hash值相等,但是又能够不出现hash冲突。这里选择预处理两个power数组,记录base1,base2的幂【双hash】,然后将质因子的下标(index)当成幂 。
CF div3 CONTEST 题目十八~二十:
第一题:
枚举 与 二分 结合【其实我说,这道题是前缀和优化枚举而已】 :【好题!!】涉及到最值得选择,第一反应是贪心,我们如果把所有物品加起来,然后再选价值小的,慢慢减,这可以吗?当然是不可以,因为涉及到两堆物品,我们无法通过贪心完全除去影响【这道题有太多特殊情况,特别是加一个大数和加一个小数在不同时刻优劣性不同。】
涉及到多情况问题,枚举是一个不错的方法,但是数据量这么大如何枚举?关注到【享受值为1的物品取走一个都是减一,享受值为2的就减2,所以我们要先从占内存大的开始取【这里就是前缀和优化啦】。所以先sort一遍,然后用lowerbound找一个合适的值就可以了,但是注意是从 0 开始。】
int pos = lowerbound(s+1,s+1+n,want) - s;
【然后lowerbound查找时,如果数组内最大的数组都没有这个want变量大,那么就会返回n+1,这个要牢记阿!!】
#include <stdio.h> #include <iostream> #include <ctime> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #include <stack> #pragma GCC optimize(3) #define re register #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cerr<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} template<typename T>inline int read(T&res){ ll x=0,f=1,flag=0;char ch; flag=ch=getchar(); if(flag==-1)return -1; while(ch<'0'||ch>'9'){ if(ch=='-')f=-1; flag=ch=getchar(); if(flag==-1)return -1; } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); flag=ch=getchar(); if(flag==-1)return -1; } res = x*f;return flag; } template<typename T,typename...Args> inline int read(T&t,Args&...a){ int res; res=read(t);if(res==-1)return -1; res=read(a...);return res; } const int maxn = 3e5+11,inf = 0x3f3f3f3f,mod=998244353; ll n,m,a[maxn],b,fir[maxn],sec[maxn]; void solve() { scanf("%lld%lld",&n,&m); ll sum = 0,ans = inf,top1=0,top2=0; for(int i = 1;i <= n;i++) read(a[i]),sum+=a[i]; for(int i = 1;i <= n;i++) read(b),(b==1?fir[++top1]=a[i]:sec[++top2]=a[i]); sort(fir+1,fir+top1+1,greater<ll>()); sort(sec+1,sec+1+top2,greater<ll>()); if(sum<m)puts("-1"); else { sum = 0; for(int i = 1;i <= top1;i++) fir[i]+=fir[i-1]; for(int i = 1;i <= top2;i++) sec[i]+=sec[i-1]; //二分要从0开始,因为可以不选 for(int i = 0;i <= top1;i++) { sum = fir[i]; //sec不用+1 int pos=lower_bound(sec,sec+1+top2,(m-sum>0?m-sum:0))-sec; if(pos<=top2&&sum+sec[pos]>=m) ans=min(ans,i+pos*2); } printf("%lld\n",(ans==inf?0:ans)); } } int main() { //这些ifdef要在main函数里面 #ifndef ONLINE_JUDGE //freopen("input.txt","r",stdin); //freopen("output.txt","w",stdout); ll sta = clock(); #endif int t = 1,kase = 1; ~scanf("%d",&t); while(t--) solve(); #ifndef ONLINE_JUDGE ll en = clock(); cerr<<"运行时间"<<(double)(en-sta)<<"ms"<<endl; #endif }
第二题:
对第一题而言,2021比2020多了1,要判断一个正数是否可以由2020和2021表示 :x = 2021a + 2020b ---> x = 2020 * (a+b)+a ,得出a<a+b,所以我们判断1的个数是不是比2020的数量少就可以了。
对第二题来说,每一个奇数都是YES【自己除自己】,那么偶数呢?偶数都能表示成 2*n -> 2*2*m --> 2*2*2*k 等等形式,总之一定就是有一个2就可以了,如果存在一个奇数divisor,那么n / m / k 中肯定是存在一个奇数分解因子的,所以,我们将所有的前缀 2 都除掉,最后检查一下 n 是不是奇数就可以了。
来几道类似的数学思维题吧,免得好像连小学数学都不会。。
练习题1 练习题2 练习3【同余最短路/扩展欧几里德】 光棍数【1的个数为偶数都可以由11整除,1的个数为3的倍数,能被三整除】
第三题【构造套路题】
矩阵异或 :进行行异或 / 列异或,看看是否最后第一个矩阵能和第二个矩阵一模一样。首先,我们只需要进行第一行元素德列异或,使得第一行元素相同,因为第一行元素已经相同,后面几行都不能进行列异或了,只能进行行异或,那么只能进行行异或【即整行取反,所以只需要判断这样行和 “ 目标矩阵 ” 的是不是一模一样 或者 完全相反即可,如果不是,就NO,检查到最后,输出YES(使用cnt数组记录第一行该列是否进行过列异或)(本来想拿二进制做的,但是数据1000了,不知道bitset行不行)】
#include <stdio.h> #include <iostream> #include <ctime> #include <cstring> #include <algorithm> #include <queue> #include <map> #include <set> #include <stack> #pragma GCC optimize(3) #define re register #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define ull unsigned long long #define fi first #define se second #define de(a) cerr<<#a<<" = "<<a<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #pragma GCC optimize(3) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) using namespace std; inline ll max(ll a,ll b){return a>b?a:b;} inline ll min(ll a,ll b){return a>b?b:a;} template<typename T>inline int read(T&res){ ll x=0,f=1,flag=0;char ch; flag=ch=getchar(); if(flag==-1)return -1; while(ch<'0'||ch>'9'){ if(ch=='-')f=-1; flag=ch=getchar(); if(flag==-1)return -1; } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); flag=ch=getchar(); if(flag==-1)return -1; } res = x*f;return flag; } template<typename T,typename...Args> inline int read(T&t,Args&...a){ int res; res=read(t);if(res==-1)return -1; res=read(a...);return res; } const int maxn = 1011,inf = 0x3f3f3f3f,mod=998244353; bool a[maxn][maxn],b[maxn][maxn]; int n,m,ans,flag,cnt[maxn]; char str[maxn]; void solve() { read(n); flag = 0; for(int i = 1;i <= n;i++) { gets(str+1); cnt[i]=0; for(int j = 1;j <= n;j++) { a[i][j]=(str[j]=='0'?0:1); } } getchar(); for(int i = 1;i <= n;i++) { gets(str+1); for(int j = 1;j <= n;j++) { b[i][j]=(str[j]=='0'?0:1); b[i][j]^=a[i][j]; } } for(int i = 1;i < n;i++) if(b[1][i]^cnt[i]!=b[1][i+1]^cnt[i+1])cnt[i+1]^=1; for(int i = 2;i <= n;i++) for(int j = 1;j < n;j++) { if(b[i][j]^cnt[j]!=b[i][j+1]^cnt[j+1]) { puts("NO"); return ; } } puts("YES"); } int main() { //这些ifdef要在main函数里面 #ifndef ONLINE_JUDGE //freopen("input.txt","r",stdin); //freopen("output.txt","w",stdout); ll sta = clock(); #endif int t = 1,kase = 1; ~scanf("%d",&t); while(t--) solve(); #ifndef ONLINE_JUDGE ll en = clock(); cerr<<"运行时间"<<(double)(en-sta)<<"ms"<<endl; #endif }
CF div2 CONTEST 题目二十一~二十二
这一次比赛的题目有较多的数学题,又感觉有点吃力了。
第一题:
ROW GCD :考虑一个性质:gcd( x , y ) == gcd( y , x - y ) == gcd( x-y , 2*y - x ) 【辗转相减】
思路来源:Nowcoder的题目:小阳的贝壳 通过用线段树维护差分数组,来求各个区间的GCD。
那么这一题也是一样,通过辗转相减,我们发现可以消掉 bj 请看:a1+bj ,a2-a1,a3-a2,a4-a3.......以此类推,如果我们预处理出差分数组的GCD,那么一切可以O(n)解决了。
第二题:【我觉得这道B题比C题难是怎么了(哭)】
Move and Turn :首先第一眼,DFS题,写了交上去,嗯,T了。。。。后来就没后来了。
题解:如果n是偶数,最远可以到达 n/2 处,若有一次向左改成向右,那么就会变成 n/2 - 2,以此类推,最后会先经过 x = 0,直到 x = -n/2 . 规律开始出来了。n为偶数时,对于横竖都一样,然后 x , y 的数量 就是 n/2 + 1。
如果n是奇数,竖方向走 n/2 + 1 或 n/2 步 ,同样,横方向也是。然后数量是:n/2 + 1或 n/2 + 2. 由于有两种可能,最后要乘2.
n&1时 : ans = 2*(n/2 + 1)*(n/2 + 2)
else : ans = (n/2 + 1)*(n/2 + 1)
至于为什么乘起来,因为组合数学【竖方向选一个点,横方向选一个点】。
CF散题练习:二十三~二十六
①E. Infinite Inversions 很好的一道题,难点在于1.需要计算逆元 2.数据范围十分之大【1~1e9】
但是也是有解决的办法的:
首先看到要计算逆元【想到树状数组计算逆元的板子】思路如下:
1.第一步先将所有的数都利用树状数组记录前缀和【即add()操作】
2.第二步从左到右遍历数组,经过一个数 a(为方便简称为a),先add(i,-1)操作删去自己,就利用树状数组求 1 ~ a 的前缀和【减掉这个数自己】,那么这个前缀和的含义就是:【在a之后出现,而且比a小的数字的数量之和,因为凡是在前面出现过的数字,我们都用add操作删掉了,那么剩下的就是存在与a相互组合的逆序对】
就这样累加即可。由于本题使用到离散化,树状数组求逆序数的方法得到了很好的运用【不然平时很难有机会用阿】
其次看到数据范围,那么大,想到了和扫描线相关的算法【即离散化】,我们发现在求逆序数时,有许多点并不需要移动,所以直接缩掉,用一个新的编号代替。【这个方法实在是太妙了。】
const int maxn = 5e5+11,mod=1e9+9; ll tr[maxn<<2],f[maxn]; ll n,m,k,top; ll a[maxn],b[maxn],d[maxn]; struct ST { ll i,v; ST(){i=v=0;} bool operator<(const ST&st)const { return v < st.v; } }s[maxn]; inline void add(ll id,ll val) { while(id <= top && id > 0) { tr[id]+=val; id += lowbit(id); } } inline ll sum(ll id) { ll sum = 0; while(id) { sum += tr[id]; id -= lowbit(id); }return sum; } inline void solve() { read(n); for(int i = 1;i <= n*2;i++) { read(s[i].v); s[i].i = i; } sort(s+1,s+1+2*n); //离散化 top = 0; for(int i = 1;i <= n*2;i++) { if(s[i].v > s[i-1].v) { if(s[i].v - s[i-1].v > 1) f[++top] = s[i].v - s[i-1].v - 1; f[++top] = 1; } if(s[i].i&1) a[s[i].i/2] = top; else b[(s[i].i-1)/2] = top; } //初始化数组【缩点之后】 for(int i = 1;i <= top;i++) d[i] = i, add(d[i],f[i]); // 提前初始化所有的前缀和 //转换 for(int i = 0;i < n;i++) swap(d[a[i]],d[b[i]]); //树状数组计算逆序数 ll ans = 0,tot = 0,tem; for(int i = 1;i <= top;i++) { add(d[i],-f[d[i]]); //减掉自己 ans += sum(d[i])*f[d[i]]; } printf("%lld\n",ans); }
②C. Searching Local Minimum :为什么那么多人说这道题水呢??明明这个二分模型就很难想到。。虽然把,看到题解之后,发现还是很有道理的。
第一点:如果一个区间 [ L,R ]上,有a[ L ] > a[ L+1 ] and a[ R ] < a[ R+1 ],那么这个区间一定存在local minimal value。
第二点:看完第一点之后,就可以二分了,只不过这一次二分,咱们不仅仅需要知道 a[ mid ],我们还要知道 a[ mid + 1 ],这样,我们才知道这个mid点是转移到 L 呢,还是转移到 R 。【主要是二分运用太刻板了,只会想query一个mid点,导致做不出】
③D1. Painting the Array I : 感觉有点类似贪心题目。
贪心的题目一般都会有一种神奇的感觉,有点借助于直觉,又有点借助于经验。如果一道题每个点的决策对前/后的影响范围很小【比如此题 ai 的去向只由 ai+1 和 两个序列的末尾数字 决定范围就很小 】或者这也是DP的思维?
④D2. Painting the Array II : 求最小值而已,做法与上面一样【思维要逆过来】
CF散题练习二十七~三十
① 数论练习题:除法分块 Floor and Mod
推理过程涉及到模除,比较容易,但是写除法分块却老是写不对。。没办法了,以后写除法分块一定要好好处理边界问题。
比如这一题,暴力法:ans += min(floor(x/(i+1)),i-1), 拆开min之后,将x/(i+1)进行分块,1~sqrt(x+1)是i-1的贡献的,
所以我们只需要处理 sqrt(x+1)+1~y 之间的贡献,【本来我是枚举系数k:1<k*(i+1)<=x 来分块的,因为一段分块区间的贡献只能是k,然后边界问题爆炸了。。后来换了写法,改成枚举(i+1),然后用nxt计算下一个位置,nxt = min(x/k-1,y):记住,一定要k-1!!!】
const int maxn = 2e5+11; ll n,m,x,y,q,k,sum[maxn]; inline void solve() { read(x,y); ll lim = sqrt(x+1),ans = 0; if((lim-1)*(lim+1) > x && lim > 0)lim--; lim = min(lim,y); ans = sum[lim]; lim++; while(lim<=y) { int k = x/(lim+1); if(k==0)break; int nxt = min(x/k-1,y); ans += (nxt - lim + 1)*k; lim = nxt + 1; } printf("%lld\n",ans); } int main() { for(int i = 2;i < maxn;i++) sum[i] = sum[i-1]+i-1; int t; cin>>t; while(t--)solve(); }
②思维题:old Floppy drive :
注意点:此题需要在一个无限循环数组里面,找出第一个前缀和 大于等于 xi 的一个位置。
所以公式就是 :公式① k*S + sum[i] >= xi 【其中S是1~n的前缀和( 即 sum[n] )】,那么ans = k*n + i - 1;
解决问题的关键,在于:
(1):维护一个1~n的最大值数组 mx[1~n] ,其中mx[i]代表 1~i 之间最大的前缀和的值。【这样做的好处在于:mx具备单调性,更加方便二分寻找一个大于等于 xi 的位置。】
(2):根据一点贪心的思想,我们要使得 k*n + i 是最小的,而且 k*S + sum[i] >= x ,很容易想到 使得 k+1【因为S是定值】,或者sum[i]尽可能大,但是如果选择 k+1 ,那么对ans的贡献是 +n,很明显比 第二个选择(+1)要差【故维护mx数组,使得sum值先是最大的】。
所以根据(2),我们先找到k的 最小值,公式② kmin = ceil { ( x - mx[n] )/S } 这样,我们确定了最小的 k。
然后,根据公式③ mx[i]min >= xi - kmin*S 根据(1),mx具有单调性,所以我们二分搜索即可。【mx[0]设置为0即可,因为x>0,即使数组a中出现了负值,对答案也不会有影响】。
const int maxn = 2e5+11; ll n,m,S,d,mx[maxn],a,b,c; inline void solve() { read(n,m); S = 0; for(int i = 1;i <= n;i++) { read(a); S += a; mx[i] = max(mx[i-1],S); } ll ans; while(m--) { read(a); if(a<=mx[n]) { ans = lower_bound(mx+1,mx+1+n,a)-mx-1; } else if(S<=0) { ans = -1; } else { ll d = a - mx[n]; ll t = ceil(1.0*d/S); a -= t*S; ans = lower_bound(mx+1,mx+1+n,a)-mx-1+t*n; } printf("%lld ",ans); } pln; }
③曼哈顿距离:Exhibition 题目要求离所有点总距离最短的点的个数 ,如果只是求一个,那么很显然就是sort后的中位数 ,
对于奇数来说,中位数只有一个,所以ans就是1;
对于偶数来说,距离最短的点在mid和mid+1之间,所以答案就是:ans=(x[mid+1]-x[mid]+1)*(y[mid+1]-y[mid]+1)【笨笨地手玩了超级久,还好没掉分】;
④C2. Guessing the Greatest (hard version):二分题:本来以为直接模拟【伪二分吗?】,根据第二大的点分成两个区间,不断这样分就可以找到,但是wa了 。后来改变思路 ,也是二分,一开始先确定最大值在secpos的左边还是右边,然后二分一个长度【len=abs(firpos-secpos)】就可以了【保险起见,我用了map保存了ask的输入,防止重复询问】
int n,pos; map<PII,int>mp; bool check(int l) { int mx = max(pos+l,pos); int mn = min(pos+l,pos); int a; if(mp[make_pair(mn,mx)]==0) { cout<<"? "<<mn<<" "<<mx<<endl; cout.flush(); cin>>a; mp[make_pair(mn,mx)] = a; } else a = mp[make_pair(mn,mx)]; if(a==pos)return 1; return 0; } int Find(int S,int E,int f) { int L,R,mid,ans=pos+f; if(f==-1) L=1,R=pos; else L=1,R=E-pos+1; while(L<R) { mid = (L+R)>>1; if(check(f*mid))R=mid,ans = pos+f*mid; else L=mid+1; } return ans; } inline void solve() { cin>>n; int t,ans; cout<<"? "<<1<<" "<<n<<endl; cout.flush(); cin>>pos; mp[make_pair(1,n)] = pos; if(pos!=n&&n!=2&&pos!=1) { cout<<"? "<<1<<" "<<pos<<endl; cout.flush(); cin>>t; }else t = pos; mp[make_pair(1,pos)] = t; if(n==2)ans = 3-pos; else if(pos==t&&pos!=1)ans = Find(1,pos,-1); else ans = Find(pos,n,1); cout<<"! "<<ans<<endl; cout.flush(); }
三十一:XOR geuss :
根据XOR的性质,如果 a 不存在 1 时,那么 a^b 之后,b上还含有1的位置会完整保留下来。
那么这道题就是这样,由于题目已经说了最大就是2^14-1,所以我们可以根据上面的做法,依次将 前7位 和 后7位 空出来,然后询问两次之后,就得到x的前七位和后七位。然后&运算一下就可以了【思维题!!】
三十二:Remainer sum :
这个很简单就想到暴力,就是从y开始,以x为公差的等差数列来取索引。【sorry - TLE】、
怎么办?我们容易想到,当 x 很大的时候,基本上 3e5/x 次也不大,但是 x 很小的时候怎么办?【毫无头绪阿】
这时候一个奇妙的做法诞生了。【分治选手走出现场!!!】
我们首先选取一个 T 作为界限,大于 T的直接暴力运算,小于T 的使用数组维护S[ i ][ j ] 表示以 i 为模数,余数为 j 的总和。
T怎么选?暴力的复杂度是O(3e5/T),数组维护是O(T),不宜偏私,使内外异法也;那么只能是T = sqrt(3e5) 了,这适合不知道数据怎么变动的情况下使用【反正O(m*√3e5) 在4秒 内不会TLE】
三十三:number of permutation:【二元组x,y序列】
很容易想到容斥。【但是我因为细节挂了。】
①如果排序之后存在相邻之间相等,那么这一串的种类数是 : ∏A(n,n) 这明明是累乘,我为什么加了起来??傻子奥
②根据上面①的思路,转化为出现的数字的【数量cnt】的累乘,不要再排序之后暴力cnt++啦,TLE不烦吗?
③根据容斥定理,x递增时,我们累计了bad的种数,y递增时,我们又累计了一次,所以最后将x,y都排序,如果排完序之后【验证check函数】,x,y序列都是递增,那么就存在减重复的情况,所以最后要加回来【好好学容斥,哭】。
④cmp函数【比较器】里面千万不要写<= / >= 啦,写 < / >就好啦,谁知道某个傻逼因为写了个<=而RE了半天呢?
const int maxn = 6e5+11,mod = 998244353; ll n,jie[maxn]; ll a[maxn],b[maxn]; struct node { int a,b; node(){a=b=0;} }s[maxn]; bool cmp(node a,node b) { if(a.a==b.a) return a.b<b.b; else return a.a<b.a; } bool check() { for(int i = 2;i <= n;i++) if(s[i-1].a>s[i].a||s[i-1].b>s[i].b) return false; return true; } inline void solve() { read(n); ll ans = jie[n]; int mx = 0; for(int i = 1;i <= n;i++) { read(s[i].a,s[i].b); a[s[i].a]++; b[s[i].b]++; mx = max(mx,s[i].a); mx = max(mx,s[i].b); } ll sum = 1,sum2 = 1; for(int i = 1;i <= mx;i++) { if(a[i]>1) sum = mul(sum,jie[a[i]],mod); if(b[i]>1) sum2 = mul(sum2,jie[b[i]],mod); } ans = ((ans-sum-sum2)%mod+mod)%mod; sort(s+1,s+1+n,cmp); if(check()) { sum = 1; for(int i = 1,j;i <= n;i=j) { j = i; while(j<=n&&s[i].a==s[j].a&&s[i].b==s[j].b) j++; sum = sum*jie[j-i]%mod; } ans = (ans+sum)%mod; } printf("%lld\n",(mod+ans)%mod); }
三十四:线段树sum query?
容易想到当有进位时,就会不平衡。所以直接维护一个线段树【这颗线段树的节点维护着l~r区间上,1~9个数位上都不为0的两个数的最小值和次小值,b[i][1]表示最小值,b[i][0]表示次小值。】
subset的意思是随意选取的子集即可,不需要连续。【我又傻逼了,还以为一定要连起来。。。。】
还有,Push_up函数比较难写,query返回的是结构体,因为返回的途中需要结合push_up【可以理解为merge函数】来合并两个区间的最佳答案。
#include <stdio.h> #include <iostream> #include <ctime> #include <iomanip> #include <cstring> #include <algorithm> #include <queue> //#include <chrono> //#include <random> //#include <unordered_map> #include <math.h> #include <assert.h> #include <map> #include <set> #include <stack> #define lowbit(x) (x&(-x))//~为按位取反再加1 #define re register #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define lld long double #define ull unsigned long long #define fi first #define se second #define pln puts("") #define deline cerr<<"-----------------------------------------"<<endl #define de(a) cerr<<#a<<" = "<<a<<endl #define de2(a,b) de(a),de(b),cerr<<"----------"<<endl #define de3(a,b,c) de(a),de(b),de(c),cerr<<"-----------"<<endl #define de4(a,b,c,d) de(a),de(b),de(c),de(d),cerr<<"-----------"<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) #define _sz(x) ((x).size()) #define PII pair<int,int> #define PLL pair<ll,ll> using namespace std; inline ll max(ll a,ll b){return (a>b)?a:b;} inline ll min(ll a,ll b){return (a>b)?b:a;} template<typename T> inline int read(T&res){ // ll x=0,f=1,flag=0;char ch; flag=ch=getchar(); if(flag==-1)return -1; while(ch<'0'||ch>'9'){ if(ch=='-')f=-1; flag=ch=getchar(); if(flag==-1)return -1; } while(ch>='0'&&ch<='9'&&flag!=-1){ x=(x<<1)+(x<<3)+(ch^48); flag=ch=getchar(); }res = x*f;return flag; } template<typename T,typename...Args> inline int read(T&t,Args&...a){ // int res; res=read(t);if(res==-1)return -1; res=read(a...);return res; } inline ll mul(ll a,ll b,ll p = 9223372036854775807){ // a %= p; b %= p; ll c = (lld)a * b / p; ll res = a * b - c * p; if (res < 0)res += p; else if (res >= p)res -= p; return res; } inline ll qpow(ll x,ll y,ll mod = 9223372036854775807){ // if(y<0)return 0; ll ans = 1; while(y>0) { if(y&1)ans = (ans*x)%mod; x = x*x%mod; y >>= 1; }return ans; } //这有三个板子read + mul + qpow const int maxn = 2e5+11,mod = 998244353; const ll inf = 0x7f7f7f7f7f; int a[maxn],n,m; struct S//0 次小,1最小 { ll b[10][2]; S(){me(b,inf);} }tr[maxn<<2]; void push_up(S&fa,S&x,S&y) { for(int i = 1;i <= 9;i++){ if(y.b[i][1] >= x.b[i][0]) { fa.b[i][1] = x.b[i][1]; fa.b[i][0] = x.b[i][0]; } else if(x.b[i][1] >= y.b[i][0]) { fa.b[i][1] = y.b[i][1]; fa.b[i][0] = y.b[i][0]; } else { fa.b[i][1] = min(x.b[i][1],y.b[i][1]); fa.b[i][0] = max(x.b[i][1],y.b[i][1]); } } } void build(int ro,int l,int r) { if(l==r){ ll t = a[l]; for(int i = 1;i <= 9;i++,t/=10){ if(t%10!=0)tr[ro].b[i][1] = a[l]; else tr[ro].b[i][1] = inf; tr[ro].b[i][0] = inf; } return ; } int mid = (l+r)>>1; build(ls,l,mid); build(rs,mid+1,r); push_up(tr[ro],tr[ls],tr[rs]); } void update(int ro,int l,int r,int x,int val) { if(l==x&&r==x){ ll t = val; for(int i = 1;i <= 9;i++,t/=10){ if(t%10!=0)tr[ro].b[i][1] = val; else tr[ro].b[i][1] = inf; } return ; } int mid = (l+r)>>1; if(x<=mid)update(ls,l,mid,x,val); else update(rs,mid+1,r,x,val); push_up(tr[ro],tr[rs],tr[ls]); } S query(int ro,int l,int r,int s,int e) { //de3(ro,l,r); if(s <= l && r <= e){ return tr[ro]; } ll mid = (l+r)>>1; S a,b,ans; if(s<=mid)a = query(ls,l,mid,s,e); if(mid<e)b = query(rs,mid+1,r,s,e); push_up(ans,a,b); return ans; } inline void solve() { read(n,m); for(int i = 1;i <= n;i++) { read(a[i]); } build(1,1,n); int x,l,r; while(m--) { read(x,l,r); if(x==1) { update(1,1,n,l,r); } else { S ans = query(1,1,n,l,r); ll P = inf; for(int i = 1;i <= 9;i++) { P = min(P,ans.b[i][1]+ans.b[i][0]); } if(P==inf) { puts("-1"); continue; } printf("%lld\n",P); } } } int main() { //mt19937 rng(chrono::steady_clock:: //now().time_since_epoch().count()); int t = 1;// int t = read(t); //cin>>t; //~scanf("%d",&t); //getchar(); while(t--) solve(); //system("pause"); }
三十五:牛牛的揠苗助长:【中位数+二分】
注意到所有的稻草都在按顺序增加。所以可以视为day天里面只有【day%n】的稻草是加1的,其他的不变。所以说,如果我们确定了day%n的大小【即余数的大小,就可以知道中位数,知道中位数,就可以知道所需要的次数】。所以枚举余数从0~n-1吗?
NO 。这样的复杂度是O(n^2*logn),logn是因为排序。会TLE的。
所以我们使用二分,复杂度为O(logn*logn*n)
单调性证明:假设K天能恰好能解决问题,那么K+1必然也行(可以把k+1天增长的那个用魔法减去),所以K天之后都是可以的。
那么小于K天的,比如K-1天,有两种可能,①有一块草和中位数差2个单位②有两块草和中位数差1个单位。那么更小的必然就不行了。
三十六:Fence great : 【DP】
这道题我试遍了其它的算法,都不会做,然后 最后想DP,发现前面的是会影响到后面的,然后就放弃了。。。。
不错,前面的确实会影响到后面的,但是这个影响是有限度的。手玩几个数据之后发现,不管如何,一个fence最多变化两次,【即一个fence有0、1、2三种状态,所以我们只要将一块板子分成三块板子来理解就行了】
定义:dp[ i ][ j ] 为第 i 块板子变化 j 次使得前1~i块板子相互不矛盾用的最少花费。
const int maxn = 4e5+11,mod = 998244353; const ll inf = 0x7f7f7f7f7f7f7f7f7f; ll n,a[maxn],b[maxn],dp[maxn][3]; inline void solve() { read(n); for(int i = 1;i <= n;i++) { read(a[i],b[i]); dp[i][1] = dp[i][0] = dp[i][2] = inf; } for(int i = 1;i <= n;i++) { for(int j = 0;j < 3;j++) for(int k = 0;k < 3;k++) if(a[i]+k!=a[i-1]+j){ dp[i][k]=min(dp[i][k],dp[i-1][j]+k*b[i]); } } ll ans = inf; for(int i = 0;i < 3;i++) ans = min(ans,dp[n][i]); printf("%lld\n",ans); }
三十七:A-B string:【构造思维题 】
看到题目只含A-B,就觉得是思维题了。但是一直没想到。
题意:寻找一个区间,区间内的每个字符都属于一个或以上的回文串。
一个区间内的所有字符都属于回文串的话,好像确实很难想。那么反过来,如果一个区间总有那么一个字符不属于回文串,那么是怎么样的呢??
我们首先找到一个区间,里面的字符都是属于回文串的: 比如: ****** ,我们现在在左边【或者右边】,加入一个字符A,变成 A******,若我们加进去之后他变成反例了,当且仅当区间里存在一个A。【因为区间只有A、B,我们从刚刚加进去的A开始,向右找,找到第一个A,比如说:A***A**,那么两个A必然形成回文串。】所以只有 A在最左侧【如果不在最侧边,必然不成立如:*A*、AA】,同时只有一个A时才成立。
const int maxn = 3e5+11,mod = 998244353; const ll inf = 0x7f7f7f7f7f; char str[maxn]; int n; inline void solve() { read(n); gets(str+1); ll ans = 1LL*n*(n-1)/2,pre=1; for(int i = 2,j;i <= n;i++) { if(str[i]==str[i-1])continue; ans++; ans -= i - pre; pre = i; } pre = n; for(int i = n-1,j;i > 0;i--) { if(str[i]==str[i+1])continue; ans -= pre - i; pre = i; } printf("%lld\n",ans); }
三十八:Salary Changing:【二分+最大中位数判断】
首先,很明显就是一道二分判断的题目,关键在于怎么判断。
易错点:
①没有明确单调区间在哪里。首先,最小的中位数一定是在排序之后的S[ mid ].L,那么我们确定了最小值,最大值就是出现过的S[ ? ].R。 然后在这个区间内跑二分,如果不是,可能会出现错误,因为区间不单调。【这里要十分注意阿,不单调的区间要搞成单调后再二分:想一下可能出现的最小值与最大值即可,想不到也没办法了(哭)】
②怎么判断?最大中位数,我们如果贪心的话,直接令 后面 n/2 + 1 位数都是mid,这样我们就能够使得中位数最大。但是有一种情况,就是S[ i ].L > median 的情况,这时,我们就只能减去S[ i ].L 了。
③累计一个cnt记录 大于等于median的数量,如果cnt已经达到n/2+1,那么后面的值直接减去S[ i ].L 即可。
贪心的思路:我们先根据S[ i ].L 来排序,小的在前,大的在后,明确一点,①大于median的L值必然被减去,②当cnt还没达到n/2+1时,小于median的减去median③cnt达到n/2+1了,直接减S[i].L;
这时,我们减的S[i].L是经过维护之后最小的。从而保证了Salary都加到median上面。
三十九:Infinite Fence【数学题】
显然与 LCM 为一个循环节。那么只需取0~lcm来算就可以了。当 r 能被 b 整除时,直接特判解决。我们要关心的
所以直接求出0~lcm之间 b 的倍数个数(不计lcm在内(记为s),然后除以 r 的倍数的数量(记lcm在内(记为t)。就是将b的倍数分成 t 份,【即 s / t 就是所求。】
交上去,WA!为什么呢?因为这是将 s 化成 t 份。【如果 s 能被 t 整除,答案就是对的,如果 s 不能被 t 整除,那么必然有一个区间的数量是 s/t + 1 。】【特判一下就好】
比如:5 14
0 5 10 14 15 20 25 28 30 35 40 42 45 50 55 56 60 65 70 【可恶阿,没有想到这个又送了一题。】
四十: Yet Another Monster Killing Problem 【贪心+思维】
看完题解之后:这个想法很绝。首先,我们需要预处理一个数组mx,其含义是:mx[ i ]代表能在一天内连续杀 i 个及以上怪物的英雄的最大攻击力!
首先把英雄按耐力越大越前来排序(题意中的耐力值)。再从 n 枚举到 1 ,途中维护一个变量 t ,t = max( t , p[ i ] ) 。
【也就是说,我们枚举到耐力值 i 时,耐力值大于 i 的所有英雄中,最大的攻击力 = t 】使用 t 来更新mx[ i ]。
vector<PII>v;// fi 是耐力,se是攻击力 sort(v.begin(),v.end(),greater<PII>()); int j = 0,t = 0; for(int i = n;i;i--) // 双指针维护mx数组就可以了,复杂度也不大。 { while(j<m&&v[j].fi>=i) t = max(t,v[j++].se); mx[i] = t; }
然后O(n)解决问题:
int j = 1;t = 0; for(int i = 1;i <= n;i++)//i表示下一次要打的是第i个怪物 { t = max(a[i],t); //维护被击杀怪物的最大攻击力,如果被杀过的怪物的攻击力比mx[j]大,说明不能连续杀j个了。 if(t > mx[j]) ans++,t=0,j=1,i--;//根据i的定义,t>mx[j]时,i还没有被击杀,所以i-- else j++; }
四十一:Berry Jam【前后缀思想+前缀和】
从中间往左右俩边走,一看就是左右分开思考。题目说剩下S和B酱要数量相等,易想到剩下的一定在左右两边。两种酱数量相等,就有一个巧妙地思维转换。把S的数字改成1,B的数字改成-1。这样把问题转换为去掉中间一个区间,左右区间的和等于0.
然后预处理左边数组的前缀和,记录sumL出现的最后的位置【这样才能使得吃的酱是最少的】
然后在计算右边的前缀和时【记得reverse,然后在sumR[ 0 ] = 0 开始】,其实就是后缀和,但我们reverse之后是前缀和。使用 "hash碰撞" sumL的位置。 ans = min(ans , 2*n - i - sumL[ sumR ]);
pre.clear(); sub.clear(); sum.clear(); ans = 2*n; pre.emp(0); for(int i = 1;i <= n;i++) { read(t); if(t==1)pre.emp(1); else pre.emp(-1); } for(int i = 1;i <= n;i++) { pre[i]+=pre[i-1]; sum[pre[i]] = i; read(t); if(t==1)sub.emp(1); else sub.emp(-1); } sub.emp(0); reverse(sub.begin(),sub.end()); for(int i = 0;i <= n;i++) { if(i)sub[i] += sub[i-1]; if(sum[-sub[i]]!=0||sub[i]==0) ans = min(ans,2*n-i-sum[-sub[i]]); }
四十二:hacker cups and ball【线段树巧妙利用+二分答案】
我的理解:对于一个序列 ,其排完序之后,数字是一定的。碍于暴力复杂度太大,所以不打算直接得到各个数字的位置,而是间接得到某一类数字的位置。接下来的思路就很巧妙了。
我们以大于等于中位数分为一类,记他们的值为1,小于的就记为0。然后排序的时候呢,就很好办了。如果是升序,那就是将所有的1放在右边,所有的0放在左边,降序相反即可。这样,我们便完成了 “分类意义上的排序”。然后最后排完序检查中位数的位置是不是1就好。【有这个思路已经很不错了。。可惜我一点不沾边】
那么如何完成上面的想法呢?排序需要知道区间内1的个数,需要有修改区间的快速方法。??这不就是线段树吗?
好了,熟悉线段树的+思路活跃的基本就过了这道题了。
const int maxn = 2e5+11; int n,a[maxn],m,L,R,mid,ans; PII p[maxn]; int tr[maxn<<2],tag[maxn<<2]; inline void push_up(int ro){tr[ro] = tr[ls] + tr[rs];} inline void build(int ro,int l,int r) { if(l==r){ tr[ro] = (a[l]>=mid); return ; } int mi = (l+r)>>1; build(ls,l,mi); build(rs,mi+1,r); push_up(ro); } inline void push_down(int ro,int l,int mi,int r) { if(tag[ro]==-1){ tag[ls] = tag[rs] = -1; tr[rs] = tr[ls] = 0; }else if(tag[ro]==1){ tag[ls] = tag[rs] = 1; tr[rs] = r-mi; tr[ls] = mi-l+1; } tag[ro] = 0; } inline void update(int ro,int s,int e,int l,int r,int v) { if(l <= s && e <= r) { tr[ro] = v*(e-s+1); tag[ro] = (v==0?-1:1); return ; } else if(r < l)return ;//////////////////// int mi = (s+e)>>1; if(tag[ro])push_down(ro,s,mi,e); if(l <= mi)update(ls,s,mi,l,r,v); if(mi < r)update(rs,mi+1,e,l,r,v); push_up(ro); } inline int query(int ro,int s,int e,int l,int r) { if(l <= s && e <= r)return tr[ro]; int mi = (s+e)>>1,ans = 0; if(tag[ro])push_down(ro,s,mi,e); if(l <= mi)ans += query(ls,s,mi,l,r); if(mi < r)ans += query(rs,mi+1,e,l,r); return ans; } bool ok() { me(tr,0),me(tag,0); build(1,1,n); for(int i = 1;i <= m;i++) { int mx = max(p[i].fi,p[i].se); int mn = min(p[i].fi,p[i].se); if(mn==mx)continue;///////////////// int sum = query(1,1,n,mn,mx); if(p[i].se > p[i].fi){ update(1,1,n,mx-sum+1,mx,1); update(1,1,n,mn,mx-sum,0); }else { update(1,1,n,mn,mn+sum-1,1); update(1,1,n,mn+sum,mx,0); } } return query(1,1,n,n/2+1,n/2+1); } inline void solve() { read(n,m); for(int i = 1;i <= n;i++) read(a[i]); for(int i = 1;i <= m;i++) read(p[i].fi,p[i].se); L = 1,R = n+1,ans=a[n/2+1]; while(L<R) { mid = (L+R)>>1; if(ok())ans=mid,L=mid+1; else R=mid; } printf("%d\n",ans); }
四十三:模板--堆维护集合的所有子集中第k大的子集之和
Dreamoon and NightMarket【优先队列】题解
一个较为直观的方法就是dfs枚举所有的物品 【取或是不取】,但是 2^n的复杂度你敢吗。
另外一个思路,使用BFS来枚举所有的物品【取或是不取】,然后使用priority_queue来优化。【BFS的优点就在于所有搜索几乎是同步进行的,所以 可以很方便地观察到下一步怎么走才是最合理】
枚举的方法:
【堆里变量的含义:pair<LL,LL> 当前和为first,该集合中所有元素中索引最大的是second 】
const int maxn = 2e5+11; ll a[maxn],n,k; inline void solve() { read(n,k); for(int i = 1;i <= n;i++)read(a[i]); sort(a+1,a+1+n); ll ans=0; priority_queue<PLL,vector<PLL>,greater<PLL> >q; q.push(mp(a[1],1)); while(!q.empty()&&k>0) { PLL it = q.top();q.pop(); if(it.fi>=ans){ ans = it.fi; k--; } if(it.se <= n) q.push(mp(it.fi+a[it.se+1],it.se+1)), q.push(mp(it.fi+a[it.se+1]-a[it.se],it.se+1)); } printf("%lld\n",ans); }
【从第一个元素开始,依次加减元素形成二叉树!!!这种枚举子集的种类的方法不错!!!】
四十四: Santa's Bot【基础概率题】
总概率由三个变量决定,即x、y、z。根据题目的思路来就好了,首先选一个小朋友,其概率为1/n,其次,这个小朋友有k个想要的礼物,我们选一个的可能是1/k,再次,合法的事件中z的小朋友必然也想要这个礼物y,所以我们累计一下每个礼物有多少小朋友想要,所以合法的概率就是:cnt[y]/n。答案就是 ∑ cnt[y]/(k*n*n)
四十五:Messenger Simulator【“动态?”前缀和!】
“动态”什么鬼东西,我乱加的,以警示世人,不要再犯我这种错误了 。
考试的时候,我就想到,维护一个数组,一开始,数组中的序列是倒序的,然后就像反向队列的一样,如果有人发消息,那么就直接把他放在数列的最后面,然后原来的位置就设置为 0,新的位置设置为1,这样求一下和就知道最远排位了。
可是事与愿违,在上面维护序列的过程,数组的大小是不断变化的,然后我本来想用 线段树/树状数组 维护后缀和的,插入元素这一操作直接打消了我的想法。
事实上,数组的大小最大是有限度的,就是n+m,所以一开始就建一个n+m的 线段树/树状数组 就好了。。。。。
四十六:Two Array【二维前缀和优化的DP / 组合数学】
定义状态:dp[2][ i ][ j ] 为第一个数列以 i 结尾,第二个数列以 j 结尾。然后 2 是滚动数组的意思。
转移方程: dp[now][ i ][ j ] = dp[pre][ i‘ <= i ][ j' >= j ] 也就是说,从上一个状态转移过来需要暴力循环求和
【其复杂度是O(n^2)】,这您是认真的吗?枚举结尾数字就要n^2了,您暴力求和还要n^2。这不得TLE阿。
事实上,我们可以一边维护前缀和,一边转移状态:【这是一时的灵感,不一定是正解,正解请看其它Blog】
for(int i = 1;i < m;i++)//m位数 { for(int j = 1;j <= n;j++) { for(int k = n;k >= 1;k--)//必须降到1才退出 { dp[pre][j][k] = (dp[pre][j][k]+dp[pre[j-1][k]+dp[pre][j][k+1]-dp[pre][j-1][k+1])%mod; if(k>=j) dp[now][j][k] = dp[pre][j][k]; } } }
四十七:Minimax Problem【状压+二分】
事实上,真的很难将这两者结合在一起阿。这里的二分跟上面的第四十二题有异曲同工之妙。首先需要注意到直接求解的难度,考虑一下验证答案呗!
那我们验证什么?验证那个最小的值呗!我们使用二分,验证时进行如下的操作:
①我们将一个序列中大于这个mid的位置设置为1,反之为0,然后用二进制存储。
②我们发现题目的求最大值其实就是互补,有点像 【与运算】 嘛。
好啦。做完了。
??怎么验证单调性??其实太刻意去验证单调性反而做不出来。如果mid不是最大的,那么它应该是存在两个序列 【与运算】之后,各个位都是1。那么我们就可以让他大一点。如果本来就不存在了,我们再增大mid,就更不可能了。
四十八:星球大战【逆向思维+并查集】
离线下来反向维护即可,①注意Find父亲时,变量不要混用。②一开始我求ans[m+1]的连通块的时候我是用DFS来求的,但是就是WA,不知道错哪了。。。。。。。。。
const int maxn = 5e5+11; const ll mod = 1e9+7; int n,m,a,b,ec; bool br[maxn]; int fa[maxn],cnt,ans[maxn],bt[maxn]; vector<int>e[maxn]; inline int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);} inline void solve() { read(n,ec); for(int i = 1;i <= n;i++)fa[i]=i; for(int i = 1;i <= ec;i++){ read(a,b); e[a].emp(b); e[b].emp(a); } read(m); for(int i = 1;i <= m;i++){ ans[i] = 1; read(a); bt[i] = a; br[a] = 1; } cnt = n-m; for(int i = 1;i <= n;i++) { if(!br[i]) for(int v:e[i]) if(!br[v]&&Find(i)!=Find(v)) fa[Find(v)] = Find(i),cnt--;; } ans[m+1] = cnt; for(int i = m;i;i--){ br[bt[i]] = 0; cnt++; for(int v:e[bt[i]]){ if(!br[v]){ a = Find(bt[i]),b = Find(v); if(a!=b){ cnt--; fa[a] = b; } } } ans[i] = cnt; } for(int i = 1;i <= 1+m;i++) printf("%d\n",ans[i]); }
四十九:Bitwise【二进制+分环+思维】
①出现了环 ---> 将数组拓展为两倍
②分块后求&运算最大值 ----->每一块都在这一位上有1才行!!!
③分 k 块 ---> 将二倍数组分成2*k块
【但是有可能出现第一个数组末尾和第二个数组开头连在一起的情况,所以只需要分成2*k-1块】
【如果不足2*k-1块,那么一定在环中分不够k块】
④根据第二点,我们贪心去判断每个数 在 第 i 位 上 存不存在 2*k-1 个 【 ones 】
合理性:由于左右两个数组是对称的,左边的分块在右边也会出现,如果有重复被使用,那么块数一定不足2*k-1
eg: a1 a2 a3 a4 a1 a2 a3 a4 【分2块】
假设现在判断到第 i 位,只有a1有,a2、a3、a4这一位是0。【假设现在答案是ans = a1 | a2 | a3,a4 = a2|a3】
【 a1 a2 a3 】【 a4 a1 】 a2 a3 a4 后面就分不下去了。
五十:WI konw【线段树】
这一题的线段树并不是建好之后直接query的,而是在求解的过程中不断更新树,从而维护到答案的正确性!!
const int maxn = 4e5+11,inf = 0x3f3f3f3f3f; int a[maxn],las[maxn],cnt[maxn]; int tr[maxn<<2],n,m,top; inline void Build(int ro,int l,int r) { tr[ro]=inf; if(l==r)return; int mid = (l+r)>>1; Build(ls,l,mid);Build(rs,mid+1,r); } inline void update(int ro,int l,int r,int p) { if(l==r){ tr[ro] = a[p]; return ; } int mid = (l+r)>>1; if(p <= mid)update(ls,l,mid,p); else update(rs,mid+1,r,p); tr[ro] = min(tr[rs],tr[ls]); } inline int query(int ro,int l,int r,int s,int e) { if(s <= l && r <= e){ return tr[ro]; }else if(r<l)return inf; int mid = (l+r)>>1,ans = inf; if(s <= mid)ans = min(ans,query(ls,l,mid,s,e)); if(mid < e)ans = min(ans,query(rs,mid+1,r,s,e)); return ans; } inline void solve() { read(n); Build(1,1,n); for(int i = 1;i <= n;i++) read(a[i]); for(int i = n;i;i--) { las[i] = cnt[a[i]]; cnt[a[i]] = i; } PII ans = mp(inf,inf); for(int i = 1;i <= n;i++) { if(las[i]){ int an = query(1,1,n,i+1,las[i]-1);//注意区间范围! ans = min(ans,mp(an,a[i])); update(1,1,n,las[i]); } } if(ans.fi!=inf) printf("%d %d\n",ans.fi,ans.se); else puts("-1"); }
五十一:(or)xor(and)【枚举每一位+组合数学】
对每一个二进制位,使用一个计数器a、b,a记录数组中这一位上有一的数的数量,b记录数组中这一位是0的数的数量!
然后就是这样的几种情况:左边是1 xor 右边是 0 ,右边是1 xor 左边是0
然后组合数学列一下就可以了!
uint n,a[maxn]; inline void solve() { read(n); uint ans = 0,tot = n*n; for(int i = 1;i <= n;i++) read(a[i]); for(uint bit = (1LL<<30);bit;bit>>=1) { int cnt = 0; for(int i = 1;i <= n;i++) if((a[i]&bit)==bit)cnt++; uint t1 = cnt*cnt; uint t2 = tot - t1; uint t3 = (n-cnt)*(n-cnt); uint t4 = tot - t3; ans += (t4*t2+t1*t3)*bit; } cout<<ans<<endl; }
五十二:平方运算【模除意义下平方出现循环节】
其实平方+模运算是一定会出现循环节的!【只不过平时模数较大,这种现象不明显】
当模数较小的时候,我们就可以把循环节计算出来!【详见getloop();】
const int maxn = 1e5+11; ll sum[maxn<<2][65],tr[maxn<<2]; bool in[maxn<<2],inc[maxn]; int vis[maxn],len[maxn<<2],pos[maxn<<2],nx[maxn],lop[maxn]; int n,m,p,a[maxn],tag[maxn<<2]; #define lcm(a,b) (1LL*a/__gcd(a,b)*b) void getloop() { for(int i = 0;i < p;i++)inc[i]=1,nx[i]=i*i%p; for(int i = 0;i < p;i++) { if(!vis[i]){ int x = i,cnt = 0;//cnt记录长度! while(!vis[x]){ cnt++; vis[x] = 1; x = nx[x]; } for(int y = i;y!=x;y=nx[y]) inc[y] = false,cnt--; //当x在环上且没有被赋值长度时,就进入循环! if(!lop[x]&&inc[x]){ int y = nx[x]; lop[x] = cnt; while(y!=x){ lop[y] = cnt; y = nx[y]; } } } } } inline void init(int ro,int v) { tr[ro] = v;in[ro] = inc[v]; if(in[ro]){ len[ro] = lop[v]; pos[ro] = 0; for(int i = 0;i < len[ro];i++,v=nx[v]) sum[ro][i] = v; } } inline void push_up(int ro) { //向上更新时! //in、tr、sum、len、pos五个数组都要更新! tr[ro] = tr[rs] + tr[ls]; if(in[ls]&&in[rs]){ in[ro] = true; len[ro] = lcm(len[ls],len[rs]); pos[ro] = 0; int P1 = pos[ls],P2 = pos[rs]; for(int i = 0;i < len[ro];i++) { sum[ro][i] = sum[ls][P1++] + sum[rs][P2++]; if(P1==len[ls])P1 = 0;if(P2==len[rs])P2 = 0; } } } inline void push_down(int ro) { //注意取模 tag[rs] += tag[ro]; pos[rs] = (pos[rs]+tag[ro])%len[rs]; tr[rs] = sum[rs][pos[rs]]; tag[ls] += tag[ro]; pos[ls] = (pos[ls]+tag[ro])%len[ls]; tr[ls] = sum[ls][pos[ls]]; tag[ro] = 0; } inline void modify(int ro) { //注意归零!取模就行 if(pos[ro]+1==len[ro])tr[ro] = sum[ro][0],pos[ro]=0; else tr[ro] = sum[ro][++pos[ro]]; tag[ro]++; } inline void Build(int ro,int l,int r) { if(l==r){init(ro,a[l]);return ;} Build(ls,l,mid_seg); Build(rs,mid_seg+1,r); push_up(ro); } inline void update(int ro,int l,int r,int s,int e) { if(s <= l && r <= e && in[ro]){modify(ro);return;} if(l==r){init(ro,nx[tr[ro]]);return ;} if(tag[ro])push_down(ro);/////////////////////////////// if(s <= mid_seg)update(ls,l,mid_seg,s,e); if(mid_seg < e)update(rs,mid_seg+1,r,s,e); push_up(ro); } inline ll query(int ro,int l,int r,int s,int e) { if(s <= l && r <= e){ return tr[ro]; } if(tag[ro])push_down(ro); ll ans = 0; if(s <= mid_seg)ans += query(ls,l,mid_seg,s,e); if(mid_seg < e)ans += query(rs,mid_seg+1,r,s,e); return ans; } inline void solve() { read(n,m,p); getloop(); for(int i = 1;i <= n;i++) read(a[i]); Build(1,1,n); int op,l,r; for(int i = 1;i <= m;i++) { read(op,l,r); if(op==1){ update(1,1,n,l,r); }else{ printf("%lld\n",query(1,1,n,l,r)); } } }
五十三:Fliptile【翻转问题】
暴力+二进制表示状态:
①直接想很难做!观察到n很小!那要不像状压DP一样,用二进制代表那个点是要改的吧!【理论支撑/直觉:只要我们枚举出第一行我们踩那些点,然后如果第一行踩完之后,还有是1的点,那么这些点只能在第二行解决!所以我们只要得到上一行的状态,就可以知道下一行要踩那些点了!(然后最后验证一下啊!)】
const int maxn = 20,inf = 0x3f3f3f3f; int n,m,k,a[maxn],b[maxn],c[maxn],ans[maxn],mn; inline bool check() { for(int i = 0;i < m;i++) if(c[i])return false; return true; } const int dx[]={0,1,-1,0,0},dy[]={0,0,0,-1,1}; inline void modify(int x,int y) { int x2,y2; for(int i = 0;i < 5;i++) { x2 = x + dx[i]; y2 = y + dy[i]; if(x2>=0&&y2>=0&&x2<m&&y2<n){ c[x2] ^= (1<<y2); } } } inline int flip(int x) { b[0] = x; int res = 0; for(int i = 0;i < m;i++) { for(int j = 0;j < n;j++) if((b[i]>>j)&1) modify(i,j),res++; for(int j = 0;j < n;j++) { if((c[i]>>j)&1) b[i+1] |= (1<<j); } } if(check())return res; else return inf; } inline void solve() { mn = inf; for(int i = 0;i < m;i++) for(int j = 0;j < n;j++) { read(k); a[i] |= (k<<j); } int lim = (1<<n); for(int i = 0;i < lim;i++) { for(int j = 0;j < m;j++) c[j] = a[j],b[j] = 0; int cnt = flip(i); if(cnt<mn) { mn = cnt; for(int t = 0;t < m;t++) ans[t] = b[t]; } } if(mn==inf){ puts("IMPOSSIBLE"); return ; } for(int i = 0;i < m;i++) { for(int j = 0;j < n;j++) { if((ans[i]>>j)&1)printf("1 "); else printf("0 "); } pln; } }
五十四:wcy的狗狗【二分的运用+双指针+排列组合+思维】
①根据题目给出的三个关键点,按照升序排序,可以看出单调性,【而且这个亲密度跟 j-i 有关,二分更加容易做了。】
②想到二分出亲密度之后,还要会排列组合,逆向思维求出亲密度小于等于mid的数量:
异色点对数=总点对数−同色点对数(O(n)做法)
总点对数: ( n − x ) ∗ x + x ∗ ( x − 1 ) / 2 (n - x) * x + x * (x - 1) / 2(n−x)∗x+x∗(x−1)/2
③最后再for循环一遍,从后往前找就是了【注意二分的范围以及同色点对的累计!】
【这个排列组合真的难死我了。。。。】
const int maxn = 1e5+11,inf = 0x3f3f3f3f; ll n,k,a[maxn],cnt[maxn]; PII ans = mp(inf,inf); ll get(ll mid) { ll tot = (n-mid)*(mid) + (mid-1)*mid/2,ans = 0; me(cnt,0); int P = 1; for(int i = 1;i <= n-mid;i++) { while(P<=i+mid&&P<=n) { //做过莫队的题目应该就能想到了 ans += cnt[a[P++]]++; } --cnt[a[i]]; } return tot - ans; } inline void solve() { read(n,k); for(int i = 1;i <= n;i++) { read(a[i]); } int L = 1,R = n,mid,want=inf; while(L<R) { mid = (L+R)>>1; ll t = get(mid); if(t<=(1LL*n*(n-1)/2)&&t>=k)R = mid,want = mid; else L = mid+1; } if(want==inf){ puts("-1"); return ; } ll num = get(want) - k;//找出从后往前第几个 for(int i = n-want;i;i--)//逆序找 { if(a[i]!=a[i+want]){ if(num==0){ ans.fi = i,ans.se = i+want; break; } num--; } } printf("%d %d\n",ans.fi,ans.se); }
五十五:树上最大中位数【二分+01状态表示】
也不是第一次使用二分+01解决中位数问题了吧,但是这一题还是想地不够深入。
一开始我以为任何时候都能左右横跳,然后使得最大中位数要么是最大值,要么是第二大值。这显然是不对的。
①我们二分一个mid,然后大于等于mid的顶点设置为1,小于mid设置为0。
②如果 连续两个节点出现1、1,那么我们通过左右横跳一定可以使得中位数更大。
③所以我们需要创造出一条没有1、1相邻的路径
④根据中位数的定义,在路径上必须有 k/2+1 个节点的值是 1 ,其余是 0。【我懵了很久,不知道怎么做,然后发现有一种DFS的方法可以验证从 s 到 t 是否存在一条路径上0、1的个数是符合中位数定义的】
⑤这一题二分区间一定要设置为【0~mx+1】,不能把mx换成1e9【否则要么TLE,要么WA】(不知道原因。。)。
void DFS(int x,int mid) { f[x] = 1; for(int v:e[x]){ if(!f[v] && max(a[v],a[x])>=mid){ DFS(v,mid); } } }
//然后最后return F[t]就可以了,只有0、1相间的情况才能从s到t【1、1相邻的情况被BFS排除了】
#include <stdio.h> #include <iostream> #include <ctime> #include <iomanip> #include <cstring> #include <algorithm> #include <queue> //#include <chrono> //#include <random> //#include <unordered_map> //#pragma GCC optimize(3,"inline","Ofast") #include <cmath> // cmath里面有y1变量,所以外面不能再声明 #include <assert.h> #include <map> #include <set> #include <stack> #define lowbit(x) (x&(-x))//~为按位取反再加1 #define re register #define mid_seg ((l+r)>>1) #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define lld long double #define uint unsigned int #define ull unsigned long long #define fi first #define se second #define pln puts("") #define deline cerr<<"-----------------------------------------"<<endl #define de(a) cerr<<#a<<" = "<<a<<endl #define de2(a,b) de(a),de(b),cerr<<"----------"<<endl #define de3(a,b,c) de(a),de(b),de(c),cerr<<"-----------"<<endl #define de4(a,b,c,d) de(a),de(b),de(c),de(d),cerr<<"-----------"<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define me(a,b) memset(a,(b),sizeof a) #define PII pair<int,int> #define PLL pair<ll,ll> #define mp make_pair using namespace std; inline ll max(ll a,ll b){return (a>b)?a:b;} inline ll min(ll a,ll b){return (a>b)?b:a;} template<typename T> inline void read(T&res){ // ll x=0,f=1;char ch; ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-')f=-1; ch=getchar(); } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); }res = x*f; } template<typename T,typename...Args>inline void read(T&t,Args&...a){read(t);read(a...);} const int maxn = 2e5+11,inf = 0x3f3f3f3f; int a[maxn],n,m,s,t,vis[maxn]; vector<int>e[maxn]; bool DFS(int x,int mid) { if(vis[x])return false; vis[x] = 1; if(x==t)return true; for(int v:e[x]){ if(!vis[v] && max(a[v],a[x])>=mid){ if(DFS(v,mid))return true; } } return false; } bool ok(int mid) { me(vis,-1); queue<int>q; q.push(s); if(a[s]>=mid)vis[s] = 1; else vis[s] = 0; while(!q.empty()) { int x = q.front();q.pop(); for(int v:e[x]) { if(vis[v] == -1){ if(a[v]>=mid)vis[v]=1; else vis[v] = 0; q.push(v); } if(vis[v]==1&&vis[x]==1) return true; } } me(vis,0); return DFS(s,mid); } inline void dfs(int x){vis[x] = 1;for(int v:e[x])if(!vis[v])dfs(v);} inline void solve() { read(n,m,s,t); int mx = 0; for(int i = 1;i <= n;i++) read(a[i]),e[i].clear(),vis[i]=0,mx=max(mx,a[i]); for(int i = 1;i <= m;i++){ int x,y; read(x,y); e[x].emp(y); e[y].emp(x); } dfs(s); if(vis[t]){ puts("YES"); int L = 0,R = mx+1,mid,ans=0; while(L<R) { mid = (L+R)>>1; if(ok(mid))ans=mid,L=mid+1; else R=mid; } printf("%d\n",ans); }else puts("NO"); } int main() { int t = 1; //cin>>t; ~scanf("%d",&t); //getchar(); while(t--) solve(); }
五十六:植物大战僵尸【二分+思维】
最大化最小值,首先想到二分最小值,那么如何验证呢?
贪心验证:直接从左到右浇水,左右横跳的方式可以给后面留出更大的步数/移动空间。由于机器人是从x=0处开始出发的,所以直接视为从1走到n,每一次走到第i位,如果bi是大于1的【即需要浇水】,那么就在浇第i位的同时,顺便浇第i+1位,这样来回横跳,浪费的水是最少的。
bool ok(ll mid) { ull sum = 0; for(int i = 1;i <= n;i++) b[i]=(mid%a[i]?mid/a[i]+1:mid/a[i]); for(int i =1;i <= n;i++) { if(i==n&&b[i]<1)break; sum++;//从上一个格子,走到这个格子 if(b[i]>1){ sum += (b[i]-1)*2;//左右 横跳 b[i+1] -= (b[i]-1);//下一个格子也被浇水了 } if(sum>m)return false; } return sum <= m; }
五十七:二维离散化【模板】
调了半天。。。
注意点:
①一定要将x、y分开离散化,得到R,C,一开始hx,hy数组不需要提前加入数字!!!【因为在加入离散化数字时,会有一个-1~1的波动,不需要加入边界!,但是如果题目说有边界,那么只需要在加入离散化的时候判断一下就好】
②然后建矩阵时,vector<vector<int>>G(R+1,vector<int>(C+1,0))【如果想要从0开始就不需要加1】
然后判断越界时:x>=0&&y>=0&&x<R&&y<C【R,C 没有等号】
class Solution { public: #define emp emplace_back #define PII pair<int,int> #define fi first #define se second #define mp make_pair const int dx[4]={0,0,1,-1}; const int dy[4]={1,-1,0,0}; const int N = 1e6; bool bound(int x){ return x>=0&&x<N; } bool bound(int x,int y,int R,int C){ return x>=0&&y>=0&&x<R&&y<C; } bool isEscapePossible(vector<vector<int>>&B,vector<int>&S,vector<int>&TT) { vector<int>hx,hy; for(auto&p:B){ for(int k=-1;k<=1;k++){ int x = p[0]+k; int y = p[1]+k; if(bound(x))hx.emp(x); if(bound(y))hy.emp(y); } } for(int k=-1;k<=1;k++){ int x = TT[0]+k; int y = TT[1]+k; if(bound(x))hx.emp(x); if(bound(y))hy.emp(y); x = S[0]+k; y = S[1]+k; if(bound(x))hx.emp(x); if(bound(y))hy.emp(y); } sort(hx.begin(),hx.end()); sort(hy.begin(),hy.end()); hx.erase(unique(hx.begin(),hx.end()),hx.end()); hy.erase(unique(hy.begin(),hy.end()),hy.end()); int R = hx.size(),C = hy.size(); vector<vector<int> >G(R,vector<int>(C,0)); for(auto&p:B){ int x = lower_bound(hx.begin(),hx.end(),p[0])-hx.begin(); int y = lower_bound(hy.begin(),hy.end(),p[1])-hy.begin(); G[x][y] = -1; } int sx = lower_bound(hx.begin(),hx.end(),S[0])-hx.begin(); int sy = lower_bound(hy.begin(),hy.end(),S[1])-hy.begin(); int ex = lower_bound(hx.begin(),hx.end(),TT[0])-hx.begin(); int ey = lower_bound(hy.begin(),hy.end(),TT[1])-hy.begin(); G[sx][sy] = -1; queue<PII>q; q.push(mp(sx,sy)); while(!q.empty()){ int x=q.front().fi,y=q.front().se,tx,ty; q.pop(); for(int i=0;i<4;i++){ tx = x + dx[i]; ty = y + dy[i]; if(bound(tx,ty,R,C)&&G[tx][ty]!=-1){ q.push(mp(tx,ty)); G[tx][ty] = -1; } if(tx==ex&&ty==ey)return true; } } return false; } };
五十八:石子游戏【差分】
这道题的类似题目之前有做过,当一个数组数字完全相等,差分数组为0。明显,我们只需要贪心地消掉差分数组就可以了。
坑点:
①m==1或n==1时特判一下!
②i%m==0时也要分开讨论,从后面往前面消,如果遇到小于0的,那么必然消不掉,return -1就可以了。【为了避免n%m==0,所以要从(n-1)/m*m开始而不是从n/m*m】
const int maxn = 5e5+11; const ll inf = 0x3f3f3f3f3f3f3f; ll n,m,d[maxn],a[maxn],mx; ll get() { ll cnt = 0; //特判1 if(m==1){ for(int i = 1;i <= n;i++) cnt += mx-a[i]; return cnt; } //特判2 for(int i = (n-1)/m*m;i >= m;i-=m) { if(d[i]>=0){ d[i-m] += d[i]; cnt += abs(d[i]); d[i] = 0; } else return -1; } //处理3 for(int i = 1;i < n;i++) { if(i+m<=n&&i%m!=0){ if(d[i]<=0){ cnt += abs(d[i]); d[i+m] += d[i]; d[i] = 0; }else return -1; } else if(d[i])return -1; } //返回结果 return cnt; } inline void solve() { read(n,m); mx = 0; for(int i = 1;i <= n;i++) { read(a[i]); d[i-1] = a[i] - a[i-1]; mx = max(a[i],mx); } d[n] = inf,d[0] = -inf; ll ans = get(); printf("%lld\n",ans); }
五十九:国王的游戏【高精+思维】
考虑两个位置之间的关系,一定要像这一题一样考虑!【luogu第一篇题解很好!】
class BigNumber{ public: const int maxn = 5e4+11;//不能超过1e5 int a[maxn],len; BigNumber():len(0){me(a,0);}; BigNumber(ll x){ len = 0; while(x){ a[len++] = x%10; x/=10; } } BigNumber(const BigNumber&b){ len = b.len; for(int i = 0;i < len;i++) a[i] = b.a[i]; } BigNumber operator=(const BigNumber&b){ len = b.len; for(int i = 0;i < len;i++) a[i] = b.a[i]; return *this; } BigNumber operator*(int num){ ll carry = 0; BigNumber ans; ans.len = len; for(int i = 0;i < len;i++){ ans.a[i] += a[i]*num + carry; carry = ans.a[i]/10; ans.a[i] %= 10; } while(carry){ ans.a[ans.len++] = carry%10; carry /= 10; } while(ans.a[ans.len]>0)ans.len++; return ans; } bool operator>(const BigNumber&b)const{ if(len==b.len){ for(int i = len-1;i>=0;i--){ if(a[i]>b.a[i])return true; if(a[i]<b.a[i])return false; } return false; } return len > b.len; } BigNumber operator/(int num){ BigNumber ans; ll temp = 0; for(int i = len-1;i>=0;i--){ temp *= 10; temp += a[i]; ans.a[i] = temp / num; if(ans.len==0&&ans.a[i]!=0){ ans.len = i; } temp %= num; } if(ans.len==0)ans.len=1; while(ans.a[ans.len]>0)ans.len++; return ans; } void put(){ while(a[len-1]==0&&len>1)len--; for(int i = len-1;i>=0;i--) printf("%d",a[i]); pln; } };
【线性基模板】
目前仍然不知道正交补有什么用,反正没考过
struct LBase { static constexpr int bit = 32; ll p[70], fail; LBase() : fail(false) { fill(p, p + bit, 0); } bool insert(ll val) { for (int i = bit - 1; ~i; i--) if ((1ll << i) & val) { if (!p[i]) return p[i] = val, true; val ^= p[i]; } return !(fail = true); } bool find(ll val) { if (val <= 0) return val < 0 ? false : fail; for (int i = bit - 1; ~i; i--) if ((1ll << i) & val) { if (!p[i]) return false; val ^= p[i]; } return true; } LBase Union(const LBase &b) { // 并集,U要大写* for (int i = bit - 1; ~i; i--) insert(b.p[i]); return *this; } friend LBase complement(LBase a) { // 求正交补[反集] LBase ret; for (int i = bit - 1; ~i; i--) for (int j = i - 1; ~j; j--) a.p[i] = min(a.p[i], a.p[j] ^ a.p[i]); for (int i = 0; i < bit; i++) { if (a.p[i]) continue; ll tmp = (1ll << i); for (int j = i + 1; j < bit; j++) if (a.p[j] & (1ll << i)) tmp += 1ll << j; ret.insert(tmp); } return ret; } friend LBase Intersection(const LBase &a, const LBase &b) { // 交集 LBase ret, d = a, all = a; // all装的是目前所有可用的低位基 // k是把baseBi和它的低位帮手削减至0所用到的baseA的异或和 for (int i = 0; i < bit; i++) { if (b.p[i] == 0) continue; ll v = b.p[i], k = 0, j; for (j = i; j >= 0; j--) { if (((1ll << j) & v)) { if (all.p[j] == 0) break; v ^= all.p[j], k ^= d.p[j]; } } if (!v) ret.p[i] = k; else { all.p[j] = v; d.p[j] = k; } } return ret; } };
六十:牛牛数数【二分+线性基】
线性基借助求第k小的数,tot-mid得到大于第k小的数有多少个!
理解:
【1】tot = (1<<cnt) - 1 + fail 【如果不存在0时,总数就是1<<cnt-1,如果有0,就需要 加1】
【2】work函数干嘛呢?其实是一个rebuild的过程,我们知道,一个序列的线性基并不是唯一的,我们通过rebuild
把 d[ i ] 上含有 1 的位置都与小于 i 的元素异或一遍,最后得到的 d[ i ] 上的 1 都是独一无二的!!
比如说:
5(4+1) 、 11(8+2+1)、21(16+4+1)
rebuild得:5、11、17
rebuild之后他们能组合成:
①5 (1,0,0)
②11(0,1,0)
③14(1,1,0)
④17 (0,0,1)
⑤20 (1,0,1)
⑥26 (0,1,1)
⑦30 (1,1,1)
我们发现rebuild完之后,一个数的排名就和二进制相关了!!【不rebuild是不存在这样的联系的,可以手玩试试!】
const int maxn = 1e5+11,inf = 0x3f3f3f3f; ll n,m,K,a[maxn],b[100],c[100],cnt,tot; bool fail; inline void insert(ll x) { for(int i = 61;i>=0;i--) if(x&(1LL<<i)){ if(b[i]) x ^= b[i]; else{ b[i] = x; return ; } } fail = true; } inline void work() { //预处理 for(int i = 0;i <= 62;i++) for(int j = 0;j < i;j++) if(b[i]&(1LL<<j))b[i]^=b[j]; // for(int i = 0;i <= 61;i++) if(b[i])c[cnt++] = b[i]; //总数 tot = (1LL<<cnt) + fail - 1; } ll k_min(ll k) { if(fail)k--; if(!k)return 0; if(k>=tot)return -1; ll ans = 0; for(int i = 0;i < cnt;i++) if(k&(1LL<<i))ans ^= c[i]; return ans; } inline void solve() { read(n,K); me(a,0),me(b,0),me(c,0); for(int i = 1;i <= n;i++){ read(a[i]); insert(a[i]); } work(); ll L = 0,R = tot,mid,ans; while(L<R) { mid = (L+R)>>1; if(k_min(mid) > K)ans=mid,R=mid; else L=mid+1; } ans = tot - ans + 1; printf("%lld\n",ans); }
六十一:幸运数字【树上LCA+线性基合并】
一看题目懵逼了很久 。
①树上倍增LCA忘了。。。【去翻了翻板子】
②woc?线性基怎么合并?在树上线性基又怎么合并?
解决方法:普通的合并其实很简单,就是将一个线性基里的元素insert进去另一个线性基里面就可以了。
【如果熟练倍增LCA的话,就更好办了】
①我们预处理LCA需要两步,(1)dfs求深度dep数组,记录up[x][0] 父节点 (2)pre函数进行DP转移!
②然后 在我们求LCA的时候会经过一条路径,我们记录这条路径就可以了!
坑点:max_element函数里面求最大值时,if((ans^d[i]) > ans) 别忘了括号!!
【我觉得自己的代码还是封装得挺好的(自恋)】
const int maxn = 2e4+11,inf = 0x3f3f3f3f; ll n,m,a,b,c,w[maxn],dep[maxn]; struct node { ll fa,d[65]; node(){fa = 0,me(d,0);} void insert(ll x) { for(int i = 62;i >= 0;i--) if(x&(1LL<<i)) if(d[i])x ^= d[i]; else{d[i] = x;return ;} } void merge(const node&s) { for(int i = 0;i <= 62;i++) if(s.d[i])this->insert(s.d[i]); } ll max_element() { ll ans = 0; for(int i = 62;i >= 0;i--) if((d[i]^ans)>ans) ans ^= d[i]; return ans; } void ini(){ fa = 0,me(d,0); } }s[maxn][20],T; vector<int>e[maxn]; void dfs(int x,int fa) { s[x][0].insert(w[x]); s[x][0].insert(w[fa]); for(int v:e[x]) if(v!=fa){ dep[v] = dep[x] + 1; s[v][0].fa = x; dfs(v,x); } } void pre() { for(int k = 1;k < 20;k++) { for(int i = 1;i <= n;i++) { s[i][k].fa = s[s[i][k-1].fa][k-1].fa; s[i][k].merge(s[s[i][k-1].fa][k-1]); s[i][k].merge(s[i][k-1]); } } } int LCA(int x,int y) { T.insert(w[x]); T.insert(w[y]); if(dep[x] < dep[y])swap(x,y); int c = dep[x] - dep[y]; for(int i = 19;i >= 0;i--) if(c&(1<<i)) { T.merge(s[x][i]); x = s[x][i].fa; } if(x==y)return x; for(int i = 19;i >= 0;i--) { if(s[x][i].fa != s[y][i].fa) { T.merge(s[y][i]); T.merge(s[x][i]); y = s[y][i].fa; x = s[x][i].fa; } } return s[x][0].fa; } inline void solve() { read(n,m); for(int i = 1;i <= n;i++) read(w[i]); for(int i = 1;i < n;i++) { read(a,b); e[a].emp(b); e[b].emp(a); } dfs(1,0); pre(); while(m--) { T.ini(); read(a,b); int lca = LCA(a,b); T.insert(w[lca]); printf("%lld\n",T.max_element()); } }
六十二:线性基能组成多少个数字 【模板:线性基】
线性基的基本性质:
线性基元素异或所能得到的数字个数 == (1<<cnt) 【cnt就是成功插入线性基的元素个数!】
六十三:生日悖论【组合数学】
首先 ∏ Cisum 算出从sum个人里,选出n组人的概率,然后再C(365,n) 从365天选取n天,最后把所有人数相同的组视为同一种元素,把日期分给组数,相当于:∏C(n,cnt[i]) 算出从n天里选一个集合给【同人数的组】,因为把人数相同的组视为同一种元素,所以这样才不会重复!
六十四:正十字【时间戳优化暴力+二分+思维】
首先,把一个正十字在障碍中移动,可以转换为【把障碍都换成正十字,然后把一个点移动到另一个点】
然后,题目要求取最大值,那么我们二分一个答案来【使用并查集维护连通性】验证就可以大大简化问题了!
最后,看到询问次数很多!然后考虑到二分的矩形图基本上变化不大,可以考虑离线下来,增加每一个矩形图和并查集的利用率!
const int maxn = 1011,inf = 0x3f3f3f3f; const int dx[4]={0,0,-1,1},dy[4]={1,-1,0,0}; int t[maxn][maxn],fa[maxn*maxn]; int n,m,a,b,c,k,now_mid; int ans[100005]; bool vis[maxn][maxn]; struct ASK { int sx,sy,ex,ey,mid,L,R,i; bool operator<(const ASK&a)const{ return mid > a.mid; } }s[100005]; vector<PII>v[maxn]; inline int Find(int x){return x==fa[x]?x:fa[x]=Find(fa[x]);} inline int get(int i,int j){return j+(i-1)*m;} inline void init_set() { for(int i = 1;i <= n;i++) for(int j = 1;j <= m;j++) fa[get(i,j)] = get(i,j),vis[i][j]=0; } inline void unite(int x,int y) { x = Find(x),y = Find(y); if(x!=y)fa[x] = y; } inline void add(int x,int y) { vis[x][y] = 1; if(y!=1&&vis[x][y-1]) unite((get(x,y-1)),Find(get(x,y))); if(y!=m&&vis[x][y+1]) unite((get(x,y+1)),Find(get(x,y))); if(x!=1&&vis[x-1][y]) unite((get(x-1,y)),Find(get(x,y))); if(x!=n&&vis[x+1][y]) unite((get(x+1,y)),Find(get(x,y))); } inline void rebuild(int len) { while(now_mid>len&&len>=0) { for(auto&p:v[now_mid]) { add(p.fi,p.se); } now_mid--; } } inline void work() { for(int i = 1;i <= k;i++) { if(s[i].L==s[i].R)continue; rebuild(s[i].mid); if(Find(get(s[i].sx,s[i].sy))==Find(get(s[i].ex,s[i].ey))) { ans[s[i].i] = now_mid; s[i].L = now_mid+1; } else s[i].R = now_mid; s[i].mid = (s[i].R+s[i].L)>>1; } } inline void solve() { read(n,m,k); me(t,inf); int ln = max(n,m); for(int i = 1;i <= n;i++) for(int j = 1;j <= m;j++) { read(a); t[i][j] = min({t[i][j],n-i+1,i,m-j+1,j}); if(a){ t[i][j] = 0; for(int k=1;i+k<=n;k++) if(k<=t[i+k][j])t[i+k][j]=k; else break; for(int k=1;i-k>0;k++) if(k<=t[i-k][j])t[i-k][j]=k; else break; for(int k=1;j-k>0;k++) if(k<=t[i][j-k])t[i][j-k]=k; else break; for(int k=1;j+k<=m;k++) if(k<=t[i][j+k])t[i][j+k]=k; else break; } if(t[i][j]>0) v[t[i][j]].emp(mp(i,j)); } for(int i = 1;i <= k;i++) { ans[i] = -1; read(s[i].sx,s[i].sy,s[i].ex,s[i].ey); s[i].i = i; s[i].L = 0,s[i].R = ((n+1)>>1)+1; s[i].mid = (s[i].L+s[i].R)>>1; } for(int i = 1;i <= 15;i++) { init_set(); now_mid = ((n+1)>>1)+1; sort(s+1,s+1+k); work(); } for(int i = 1;i <= k;i++) printf("%d\n",ans[i]); }
这个问题的实现是很困难的,主要难在:
①不知道怎么样将这么多个点同时二分!
②时间戳在变化的时候,我们应该怎么维护这个矩形图
【题解】
一开始维护一个t数组【表示什么时候这个点可以加入并查集】我们把时间戳从高到低不断地减下来,不断地add点,【因为并查集加点比删点简单,所以考虑从高到低】,然后所有的询问都自己做自己的二分,互相之间并无联系,只是增加了同一个图的利用率而已!
六十五:CCF:窗口【排序题】
不要想着O(n^2)去模拟,而是直接判断 点击的点 从顶层到底层逐一判断在不在窗口里面即可!
六十六:NOIP提高组~列队【01线段树 / 01树状数组排序】
本题涉及到线段树的动态开点,和【四十五题】有点相似,但是此题更难,因为涉及到n个序列,同时还有第m列分开考虑。
【不过这些东西在动态开点之下都显得很简单了!!动态开点牛逼!!】
【还有 l ~ r 的含义真的很妙,根据cnt的不同,得到的l~r是不一样的!!!但是都是正确的排序!!】
const int maxn = 5e5+11,maxm = 1e7+11; ll n,m,q,x,y; struct seg_tree { ll id,cnt; int ls,rs; seg_tree(ll _cnt=0){ ls=rs=id=0; cnt = _cnt; } }tr[maxm]; int root[maxn],top,used[maxn]; inline ll query(int ro,int l,int r,ll p,ll&res,bool tp) { if(l==r){ if(tp){ res = (tr[ro].id?tr[ro].id:l*m);//L才是当前的排名 }else{ res = (tr[ro].id?tr[ro].id:(x-1)*m+l);//同理L } return l; } if(!tr[ro].ls){ tr[ro].ls = ++top; tr[top] = seg_tree(mseg-l+1); tr[ro].rs = ++top; tr[top] = seg_tree(r-mseg); } if(tr[tr[ro].ls].cnt >= p) return query(tr[ro].ls,l,mseg,p,res,tp); else return query(tr[ro].rs,mseg+1,r,p-tr[tr[ro].ls].cnt,res,tp); } inline void modify(int ro,int l,int r,ll p,ll id,ll v) { if(l==r){ tr[ro].id = id; tr[ro].cnt = v; return ; } if(!tr[ro].ls){ tr[ro].ls = ++top; tr[top] = seg_tree(mseg-l+1); tr[ro].rs = ++top; tr[top] = seg_tree(r-mseg); } if(p <= mseg)modify(tr[ro].ls,l,mseg,p,id,v); else modify(tr[ro].rs,mseg+1,r,p,id,v); tr[ro].cnt = tr[tr[ro].ls].cnt + tr[tr[ro].rs].cnt; } inline void solve() { read(n,m,q); for(int i = 1;i <= n;i++){ root[i] = ++top; tr[top] = seg_tree(m-1); } root[n+1] = ++top; tr[top] = seg_tree(n); for(int i = 1;i <= q;i++) { read(x,y); ll res,cur,p1,p2; if(y!=m){ //p记录原来的位置,然后放入modify中删掉 p1 = query(root[x],1,m+q,y,cur,0); p2 = query(root[n+1],1,n+q,x,res,1); modify(root[x],1,m+q,m+used[x]++,res,1); modify(root[n+1],1,n+q,n+i,cur,1); modify(root[x],1,m+q,p1,0,0); modify(root[n+1],1,n+q,p2,0,0); }else{ p1 = query(root[n+1],1,n+q,x,cur,1); modify(root[n+1],1,n+q,p1,0,0); modify(root[n+1],1,n+q,n+i,cur,1); } printf("%lld\n",cur); } }
六十七:选择客栈【组合数学】
const int maxn = 2e5+11,inf = 0x3f3f3f3f; int n,m,pr,p[maxn],id[maxn]; ll cnt[111],sum[111],used[111],ans; inline void solve() { read(n,m,pr); for(re int i = 1;i <= n;i++){ read(id[i],p[i]); sum[id[i]]++; } for(re int i = 1;i <= n;++i){ ++cnt[id[i]];//一定要先加!! if(p[i]<=pr){ for(re int j = 0;j <= 100;++j){ //特判当前点的色调!!! ans += (cnt[j]-(id[i]==j))*(sum[j]-cnt[j]-used[j]); if(j==id[i]){ ans += sum[j]-used[j]-1; } used[j] += cnt[j]; cnt[j] = 0; } } } printf("%lld\n",ans); }
六十八:篝火晚会【环的知识+思维+贪心】
①结论一:如果能形成一个环,贪心地组环就可以了。
②首先需要贪心地想到,如果需要代价最小,我们需要找到【原始环】与【目标环】匹配点最多的位置。因为我们总有方法使得已经匹配的点不动,然后只动那些未匹配的点【所以结论二:answer = n - 已匹配点】
③要使得匹配点最多,咱们就需要用到两个结论:
- 【1】如果两个环能通过左右移动来使得它们完全一致,那么环Ⅰ 映射到 环Ⅱ 上的点的距离必须一致。 即:| ai - bi | = const_number 这个值就是需要左右移动的距离
- 【2】如果两个环能通过反转+移动使得他们完全一致,那么环Ⅰ上点的位置 + 环Ⅱ上对应点的位置 = 一个常数。
即 ( ai + bi )%n = const_number
所以只要(1)建环 (2)两次求最大值 就可以得到答案了。
六十九:宝藏【状压DP / dfs搜索】
①DP:
小技巧:假设s是一个利用二进制表示的集合,那么如何枚举它的所有子集呢?
for(int i=s;i;i=(i-1)&s)
②dfs
七十:Placing Medals on a Binary Tree(CF)
①把问题转换成1- 1/2 - 1/4 .... 直到减不动了,那么这个点就加不进去
②但是又出现了一个新的问题,浮点数精度不够,直接模拟高精的话又会TLE
③考虑到是一个完全二叉树的图形,我们把问题转换到二进制上面,变成【使用map】模拟二进制
然后如果一个深度存在两个节点,那么就进位到上一个节点。
④如果之前1、2、3、4、5 都出现过了,或者因为进位而出现了,那么我们再加一个5进去,这一颗树就满了【全部进位到0节点】,但是如果1、2、3、4、5都出现过,还出现过1000,那么5也就塞不进去了【也就是说,当进位到0的情况下,5应当是最大值】
根据这个特殊的情况,我们定义 mx 为从 1 ~ mx 连续出现的序列【如1、2、3、4、5、6,mx为 6】
分类如下:
【1】显然,当进位到0、或者当前要放的medals的值小于mx 都是无法放入的,直接输出No
【2】当mx == medals 时,判断一下mx是不是等于maxx【之前的最大值】
【3】否则必然可以输出Yes
最后 ①更新一下 maxx,mx的 ②进位操作 就可以了
ll n,a,flag,mx,maxx; unordered_map<int,int>has; inline void solve() { read(n); for(int i = 1;i <= n;i++) { read(a); maxx = max(maxx,a); if(has[0] || mx > a || a == 0){ puts("No"); continue; } if(mx == a && maxx > a){ puts("No"); continue; } else{ has[a]++; while(a > 0){ if(has[a] == 2){ has[a] = 0; has.erase(has.find(a)); has[--a]++; } else break; } while(has[mx+1])++mx; puts("Yes"); } } }
七十一:Hidden Anagrams(CF)
。。。挺巧妙的一个优化 从O(n^3) 降到 O(n^2*logn)
还记得之前的等式对碰嘛?【x+y=0 只需要枚举x,然后用y去对碰,从而使O(n^2)降到O(n)】
这一题也是一样,我们使用一个map标记一下子串,然后用第二个字符串去对碰一下。
【这里hash_map装的是一个struct,表示前缀和差分之后的子串字母的数量,然后写一个operator<就可以实现logn的查找了】
char s1[maxn],s2[maxn]; struct node{ int a[30]; node(){me(a,0);}; bool operator<(const node&t)const{ for(int i = 0;i < 26;i++){ if(a[i] < t.a[i])return true; if(a[i] > t.a[i])return false; } } }; map<node,bool>has; int n,m,lim,ans; inline void solve() { scanf("%s%s",s1+1,s2+1); n = strlen(s1+1),m = strlen(s2+1),lim = min(n,m); for(int i = 1;i <= lim;i++){ has.clear(); node tmp,tmp2; for(int j = 1;j <= n;j++){ if(j < i){ tmp.a[s1[j]-'a']++; }else{ tmp.a[s1[j]-'a']++; if(j>i)tmp.a[s1[j-i]-'a']--; has[tmp] = 1; } } for(int j = 1;j <= m;j++){ if(j < i){ tmp2.a[s2[j]-'a']++; }else{ tmp2.a[s2[j]-'a']++; if(j>i)tmp2.a[s2[j-i]-'a']--; if(1 == has[tmp2]){ ans = i; break; } } } } printf("%d\n",ans); }
七十二:H - Animal Companion in Maze(CF)
这个题哭了【调了一天】
思路:
① 本图有【有向边、无向边】两种边,之前做过一道使用分层图做的,但是这道题求最长路径,不能这样搞
② 然后首先解决一个问题,如果不存在Infinite的情况,那么这个图就可以视为一个森林:
双向边连通块是树,然后 单向边是连着这些树的通道
所以我们就需要把树给分类出来,怎么分呢?树是双向边,使用tarjan缩点就可以给每个树一个编号啦!【之前以为tarjan只能缩环,没想到在DAG+树的图中,还能把树给抽出来】
③那么tarjan缩点之后呢?我们知道 环 对 tarjan缩点是没有影响的,所以一开始直接tarjan缩点就可以了,然后来一个check_circle函数,因为环在tarjan中已经缩掉了,所以直接从上面得到的环开始搜索就可以了。
如何判断有无环【前提是,遍历时只能遍历属于同一个连通块的点】:
显然,如果之前走过的点又被遍历到,那么就是有环【不走父节点fa!=v的情况下】
如: 2 2 1 2 2 1 2 2 【test①】
④判环完成之后,rebuild一下这个图,常规操作了,不过需要记录这条边的左右两个节点,方便后续DP【此时这些边一定是单向边】
⑤DP环节:
我们定义一个状态:dp[ i ]表示以 i 节点结尾的路径最长有多长。
【1】首先需要【拓扑排序】地遍历每一个缩点,这样我们就能保证所有进入这个缩点的dp[ i ]数组的值都是更新完毕的,这时候,我们就可以对这个联通缩点进行两次DFS
【2】第一次DFS,像求树上直径那样,求一个最深路径、次深路径
【3】第二次DFS,根据上述的fir、sec数组【从上往下】地更新dp数组,转移方程如下:【i是该节点,fa是父节点】
if(fir[fa] == fir[i] + 1) dp[i] = max({sec[i]+1,in[fa]+1,dis+1}); else dp[i] = max({fir[i]+1,in[fa]+1,dis+1}); ①dis 是从上面往下累计的最大值 ②in 数组是在BFS时记录的该节点最长【进入路径】 如: 3 -> 2 -> 1 那么in[1] = 2,in[2] = 1
其实解决本题的关键就三个:
①想出森林+有向边相连的模型->②知道缩点【缩点取树】并且会判环->③会定义DP状态
const ll maxn = 1e5+11; int n,m,top,dfn[maxn],ins[maxn],ind,flag,head[maxn],root[maxn]; int fir[maxn],sec[maxn],du[maxn],low[maxn],id[maxn],scc,h2[maxn]; int in[maxn],ans; stack<int>s; struct scc_edge{int to,a,b,next;}fe[maxn<<2]; struct edge{int to,i,next;}e[maxn<<2]; inline void add(int x,int y,int i){ e[++top] = {y,i,head[x]}; head[x] = top; } inline void add(int x,int y,int a,int b){ fe[++top] = {y,a,b,h2[x]}; h2[x] = top; } inline void check_circle(int x,int fa) { if(low[x]){flag = true;return ;} low[x] = 1; for(int i = head[x],v;i;i=e[i].next) if(id[e[i].to]==id[x]){ v = e[i].to; if(fa!=v)check_circle(v,x); } } inline void dfs(int x){ s.push(x); ins[x]=dfn[x]=low[x]=++ind; for(int i = head[x];i;i=e[i].next){ int v = e[i].to; if(!dfn[v]){ dfs(v); low[x] = min(low[x],low[v]); }else if(ins[v]){ low[x] = min(low[x],dfn[v]); } } if(low[x]==dfn[x]){ scc++;int v; root[scc] = x; do{ v = s.top();s.pop(); ins[v] = 0; id[v] = scc; }while(x!=v); } } inline int dfs_1(int x) { if(dfn[x])return fir[x]; dfn[x] = 1;fir[x] = max(fir[x],in[x]); for(int i = head[x],v;i;i=e[i].next) if(!dfn[e[i].to] && id[x] == id[e[i].to]){ v = e[i].to; int d = dfs_1(v) + 1; if(d > fir[x]){ sec[x] = fir[x]; fir[x] = d; }else sec[x]=max(sec[x],d); } //不需要ans=max(sec[x]+fir[x],ans);以防如下数据 //那么树上直径就是答案怎么办? //哈哈,不怕,dp时可以解决 /* 4 5 1 2 1 1 3 1 1 4 1 2 3 2 4 3 2 */ return fir[x]; } inline void dfs_2(int x,int dis) { if(low[x])return ; low[x] = 1; for(int i = head[x],v;i;i=e[i].next) if(!low[e[i].to]&&id[x]==id[e[i].to]){ v = e[i].to; if(fir[v]==fir[x]-1){ dfs_2(v,max({dis+1,sec[x]+1,in[x]+1})); }else{ dfs_2(v,max({dis+1,fir[x]+1,in[x]+1})); } } in[x] = max({in[x],fir[x],dis});//这里为了省内存,直接把in代替dp数组 } inline void solve() { read(n,m); for(int i = 1,t1,t2,op;i <= m;i++){ read(t1,t2,op); add(t1,t2,i); if(op==2)add(t2,t1,i); } me(ins,0),me(low,0); for(int i = 1;i <= n;i++)if(!dfn[i])dfs(i); //check_circle me(ins,0),me(low,0); for(int i = 1;i <= scc;i++)if(!low[i]){ check_circle(i,0); if(flag){ puts("Infinite"); return ; } } //rebuild top = 0; for(int x = 1;x <= n;x++){ for(int i = head[x],v;i;i=e[i].next){ v = e[i].to; if(id[v] != id[x]) add(id[x],id[v],x,v),du[id[v]]++; } } me(dfn,0),me(low,0); //BFS过程 queue<int>q; for(int i = 1;i <= scc;i++) if(!du[i])q.push(i),dfs_1(root[i]),dfs_2(root[i],0); while(!q.empty()){ int x = q.front();q.pop(); for(int i = h2[x],v;i;i=fe[i].next){ v = fe[i].to; du[v]--; in[fe[i].b] = max(in[fe[i].b],in[fe[i].a]+1); if(du[v]==0){ //开始两次DFS q.push(v); dfs_1(root[v]); dfs_2(root[v],0); } } } for(int i = 1;i <= n;i++) ans = max(ans,in[i]);//,de(in[i]); printf("%d\n",ans); }
七十三:Symmetric Matrix【牛客】
一道邻接矩阵的构造题+组合数DP+数学推算化简公式
总结:
①矩阵上特殊的值,看看能不能视为邻接矩阵,本题中一行 有两个1 / 一个2,而且还是对称矩阵,说明A有一条指向B的边,B一定有一条指向A的边,所以2个点的环、两个以上点的环都可以表示出来。
②圆排列:k个节点形成的环有多少种情况? answer = (k-1)! / 2
由于我们选取k个数与第n个节点形成环的话,又存在多种情况,根据圆排列,一共有k+1个节点,所以是 k!/ 2 种情况
【题解】
七十四:小G的GCD【牛客】思维题
最终状态是:(1,0)时最优,然后倒数第二步是:(2,1)时最优,为了使得辗转相除的次数最多,当我们得到一个状态(x,y)时,我们可以构造出(y+x,x)是它的上一步状态,然后(x*2+y,y+x),再然后是(x*3+y*2,x*2+y)
即答案最大时,(A,B)的上一步应该是(B+A,A)
得到:
[ f(x+1) ,f(x) ] 的前一个状态是 [ f(x+1)+f(x), f(x+1) ] 令f(x+2) = f(x+1) + f(x)
咦?这不是斐波那契数列嘛? 所以我们求斐波那契数列中小于n的最大一项最好了!
const int maxn = 111,inf = 0x3f3f3f3f,mod = 1e9+7; ull n,sum,a[maxn]; //gcd 最大沿着类似log2的方式递增,所以111够了 inline void solve() { read(n); if(n==1){//特判 printf("2\n"); return ; } a[0] = a[1] = 1; for(ull i = 2;i;i++){ a[i] = a[i-1] + a[i-2]; if(a[i] > n){ printf("%d\n",i); return ; } } }
七十五:Simone and Graph Coloring【2021昆明站ICPC】
这道题手糊一下发现需要的颜色就是最长递减子序列,然后lower_bound维护一下就好了
但是还要输出每个节点的颜色,这该怎么办?
我们发现,每一个节点都可以存在一个递减序列里面【任意一个即可】,而这个递减序列的长度必然小于等于max_length【最长的递减子序列的长度】
所以这个节点的颜色编号只需要贪心地在【离他最近的、在他前面的、ai 大于他的】节点的颜色上加一即可。
比如数据:7 , 7 3 2 6 5 1 4
形成的最长递减子序列一定相互有边,所以直接互不相同。
然后这样想:如果在第i个数之前有比 ai 大的数,那么我们就可以构建出包含 ai ,并且以 ai 结尾的最长递减子序列,这个时候,我们把这个子序列的倒数第二项作为 ai 的 father 即可,输出的时候在 col[ father ] 基础上直接加一。
【橙色部分很重要,是贪心的关键思想,至于怎么实现呢?我们在维护最长子序列的过程中顺便更新一下就好】
const int maxn = 1e6+11,inf = 0x3f3f3f3f,mod = 1e9+7; int n,a[maxn],ans,fa[maxn],col[maxn],mn[maxn],top; inline void solve() { read(n); for(int i = 1;i <= n;i++){ read(a[i]); col[i] = fa[i] = 0; } top = 0; for(int i = 1;i <= n;i++){ if(top==0||mn[top]>a[i]){ if(top)fa[a[i]] = mn[top]; mn[++top] = a[i]; }else{ int pos = lower_bound(mn+1,mn+1+top,a[i],greater<int>()) - mn; if(pos>1&&mn[pos-1]<a[i])pos--; mn[pos] = a[i]; if(pos > 0){ fa[a[i]] = mn[pos-1]; } } //for(int j = 1;j <= top;j++){ // cout<<mn[j]<<" "; //}pln; } ans = top; for(int i = 1;i <= n;i++){ de(a[i]<<" "<<fa[a[i]]); col[a[i]] = (col[fa[a[i]]])%ans + 1; } for(int i = 1;i <= n;i++){ printf("%d ",col[a[i]]); }pln; } int main() { //freopen("P5022_23.in","r",stdin); int TEST = 1; //cin>>TEST; ~scanf("%d",&TEST); //getchar(); while(TEST--) solve(); }
七十六:对称二叉树【牛客】暴力搜索、递归妙用
首先题目给的是一颗二叉树,但是完全有可能退化成两条对称的链,所以我们没有办法将高度降到logn级别
然后只能在这棵树上搜索了:
(1)题目要求每一棵子树是从根到它的所有子节点,也就是说,如果从上往下搜索时,出现了一颗对称的,那么就不需要再往下搜索了。所以直接将size大的排序,从上往下贪心地搜【优化一】
(2)然后关于暴力搜索的设计:【u是root的左节点,v是root的右节点】
check(son[u][0],son[v][1]) && check(son[u][1],son[v][0]) 左和右同时进行对称check即可
我也不知道为什么这个复杂度过得去(苦笑),反正我算出来【极端情况下】会被卡。
int n,son[1000050][2],val[1000050],size[1000050]; int id[1000011]; //son[i][0]为i的左儿子 //son[i][1]为i的右儿子 inline void dfs(int u) { size[u]=1; if (son[u][0]!=-1) { dfs(son[u][0]); size[u]+=size[son[u][0]]; } if (son[u][1]!=-1) { dfs(son[u][1]); size[u]+=size[son[u][1]]; } } inline bool check(int u,int v) { if (u==-1 && v==-1) return true; if (u!=-1 && v!=-1 && val[u]==val[v] && check(son[u][0],son[v][1]) && check(son[u][1],son[v][0])) return true; return false; } bool cmp(int x,int y){ return size[x] > size[y]; } int main() { n=read(); for (int i=1;i<=n;i++) val[i]=read(),id[i]=i; for (int i=1;i<=n;i++) { son[i][0]=read(); son[i][1]=read(); } dfs(1); sort(id+1,id+1+n,cmp); int ans=0; for (int i=1;i<=n;i++){ int ti = id[i]; if (check(son[ti][0],son[ti][1])){ ans=max(ans,size[ti]); break; } } cout << ans << endl; return 0; }
我觉得最重要的是:①有递归的想法②大胆去尝试
七十七:最小生成树【模板与教训】
经过这一道题之后,我深刻地明白:如果要使用【最小生成树】算法/思想,必须要严格按照边权从小到大加起来【或者从大到小】
我一开始想着.... 不排序,从小的点开始计算
七十八:Gentle Jena【单调栈+DP】
说实话,DP的题目有灵感就好做了
#include <stdio.h> #include <iostream> #include <ctime> #include <iomanip> #include <cstring> #include <algorithm> #include <queue> #include <bitset> //#include <chrono> //#include <random> //#include <unordered_map> //#pragma GCC optimize(3,"inline","Ofast") #include <cmath> // cmath里面有y1变量,所以外面不能再声明 #include <assert.h> #include <map> #include <set> #include <stack> #define lowbit(x) (x&(-x))//~为按位取反再加1 //#define re register #define mseg ((l+r)>>1) #define ls (ro<<1) #define rs ((ro<<1)|1) #define ll long long #define lld long double #define uint unsigned int #define ull unsigned long long #define fi first #define se second #define pln puts("") #define deline cout<<"-----------------------------------------"<<endl #define de(a) cout<<#a<<" = "<<a<<endl #define de2(a,b) de(a),de(b),cerr<<"----------"<<endl #define de3(a,b,c) de(a),de(b),de(c),cerr<<"-----------"<<endl #define de4(a,b,c,d) de(a),de(b),de(c),de(d),cerr<<"-----------"<<endl #define emp(a) emplace_back(a) #define iter(c) __typeof((c).begin()) #define ios ios::sync_with_stdio(0),cin.tie(0),cout.tie(0) #define PII pair<int,int> #define PLL pair<ll,ll> #define me(x,y) memset((x),(y),sizeof(x)) #define mp make_pair using namespace std; //template<class T>inline void fill(T*p,T v,re int n){while(n--)p[n]=v;} template<class T,class F>inline F max(T a,F b){return (a>b)?a:b;} template<class T,class F>inline F min(T a,F b){return (a>b)?b:a;} template<typename T> inline void read(T&res){ // ll x=0,f=1;char ch; ch=getchar(); while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} res = x*f; } template<typename T,typename...Args>inline void read(T&t,Args&...a){read(t);read(a...);} const int maxn = 1e7+11,inf = 0x3f3f3f3f,mod = 998244353; ll n,m,b,ans,x,y,z,p,A[maxn],L[maxn],top; PLL s[maxn]; inline void update(ll x,ll p) { while(top>0&&s[top].fi>=x)top--; if(top>0) L[p] = (L[s[top].se]+(p-s[top].se)*x%mod)%mod; else L[p] = x*p%mod; s[++top] = mp(x,p); } inline void solve() { read(n,p,x,y,z,b); L[1] = A[1] = b; s[++top] = mp(b,1); for(int i = 2;i <= n;i++){ b = (A[i-1]*x%p+b*y%p+z)%p; update(b,i); A[i] = (L[i] + A[i-1])%mod; } for(int i= 1;i <= n;i++){ ans ^= A[i]; } printf("%lld\n",ans); } int main() { //freopen("P5022_23.in","r",stdin); int TEST = 1; //cin>>TEST; //~scanf("%d",&TEST); //getchar(); while(TEST--) solve(); }
七十九:Lottery Tickets【贪心+构造】
该题难在如何构造后面两位数,使得整个串最大!【反正就是细节一大堆wdnmd】
贪心的思路:【我们到底如何做,才能构造出正确的最后两位呢?】
{0,20,12,32,40,4,24,44,52,60,16,36,64,56,72,76,80,8,28,84,48,68,88,92,96} 按照这个数组去查找就行了【真的!!】
(1) 比较 个位 和 十位 中最大值的大小 【这一点很重要,决定了小的数先出场,保证大的数不会浪费在最后两位】
(2) 如果相同,就比较个位的大小 【这一点也很重要,保证了所有合法的串中,最后一位是最小的!】
(3)如果还相同,就比较十位大小。
该数组是这样得来的:然后这样找就能保证找到之后,整个串还是最大的!!
int a[20],suf1,suf2; inline bool ok(){ int ans = -1; for(int i = 0;i < 10;i++){ if(a[i]>0) for(int j = 0;j <= i;j++){ if(a[j]==0)continue; if(i==j&&a[i]<2)continue; if((i*10+j)%4==0){ a[j]--,a[i]--; suf1 = i,suf2 = j; return true; } if((j*10+i)%4==0){ a[j]--,a[i]--; suf1 = j,suf2 = i; return true; } } } for(int i = 0;i < 10;i+=4) if(a[i])suf1 = i; return false; } inline void solve() { me(a,0); bool flag = 0; for(int i = 0;i <= 9;i++) { read(a[i]); if(i&&a[i])flag=1; } suf1=suf2=-1; if(!flag){ if(a[0])puts("0"); else puts("-1"); return ; } if(!ok()){ printf("%d\n",suf1); }else{ for(int i = 9;i >= 0;i--){ while(a[i]>0){ printf("%d",i); a[i]--; } } printf("%d%d\n",suf1,suf2); } }
八十:2020-2021 ICPC - Gran Premio de Mexico - Repechaje - I. Integers Rectangle Challenge 【CF gym】
①首先想最小子矩阵怎么做:
我们枚举 : (a,b,c,d)为左/右边界点,这样可以吗?的确可以,但是怎么转移?
我们换个方向 : (h,w,i,j)
- 以(i-h+1)为上边界,以i为下边界
- 以(j-w+1)为左边界,以j为右边界
然后,将 h,w 从小枚举到大,这样转移:
//a b c d 最大子矩阵 //me(mx,-inf);//可有可无 for(int h = 1;h <= n;h++) for(int w = 1;w <= m;w++) for(int i = h;i <= n;i++) for(int j = w;j <= m;j++){ mx[h][w][i][j] = getSum(i-h+1,j-w+1,i,j); if(h > 1){ mx[h][w][i][j] = max(mx[h][w][i][j],mx[h-1][w][i][j]); mx[h][w][i][j] = max(mx[h][w][i][j],mx[h-1][w][i-1][j]); } if(w > 1){ mx[h][w][i][j] = max(mx[h][w][i][j],mx[h][w-1][i][j]); mx[h][w][i][j] = max(mx[h][w][i][j],mx[h][w-1][i][j-1]); } }
②这道题基本上就做好了,剩下的就是如何划分的问题了,划分方式有6种,枚举即可。【这六种划分看看代码就行】
const ll maxn = 55,inf = 0x3f3f3f3f3f,mod = 998244353; ll sum[maxn][maxn],n,m,mx[maxn][maxn][maxn][maxn],ans; inline ll getSum(int a,int b,int c,int d){ return sum[c][d]-sum[a-1][d]-sum[c][b-1]+sum[a-1][b-1]; } inline ll getMax(int a,int b,int c,int d){ return mx[c-a+1][d-b+1][c][d]; } inline void init(){ //二位前缀胡 for(int i = 1;i <= n;i++) for(int j = 1;j <= m;j++) sum[i][j] += sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]; //a b c d 最大子矩阵 //me(mx,-inf);//可有可无 for(int h = 1;h <= n;h++) for(int w = 1;w <= m;w++) for(int i = h;i <= n;i++) for(int j = w;j <= m;j++){ mx[h][w][i][j] = getSum(i-h+1,j-w+1,i,j); if(h > 1){ mx[h][w][i][j] = max(mx[h][w][i][j],mx[h-1][w][i][j]); mx[h][w][i][j] = max(mx[h][w][i][j],mx[h-1][w][i-1][j]); } if(w > 1){ mx[h][w][i][j] = max(mx[h][w][i][j],mx[h][w-1][i][j]); mx[h][w][i][j] = max(mx[h][w][i][j],mx[h][w-1][i][j-1]); } } } inline ll slice_X(int L,int R){ ll mx = -inf; for(int i = 1;i < m;i++) mx = max(mx,getMax(L,1,R,i)+getMax(L,i+1,R,m)); return mx; } inline ll slice_Y(int L,int R){ ll mx = -inf; for(int i = 1;i < n;i++) mx = max(mx,getMax(1,L,i,R)+getMax(i+1,L,n,R)); return mx; } inline void solve() { ans = -inf; read(n,m); for(int i = 1;i <= n;i++){ for(int j = 1;j <= m;j++){ read(sum[i][j]); } } init(); for(int i = 1;i < n;i++){ ans = max(ans,slice_X(1,i)+getMax(i+1,1,n,m)); ans = max(ans,slice_X(i+1,n)+getMax(1,1,i,m)); for(int j = i+1;j < n;j++){ ans = max(ans,getMax(1,1,i,m)+getMax(i+1,1,j,m)+getMax(j+1,1,n,m)); } } for(int i = 1;i < m;i++){ ans = max(ans,slice_Y(1,i)+getMax(1,i+1,n,m)); ans = max(ans,slice_Y(i+1,m)+getMax(1,1,n,i)); for(int j = i+1;j < m;j++){ ans = max(ans,getMax(1,1,n,i)+getMax(1,i+1,n,j)+getMax(1,j+1,n,m)); } } printf("%lld\n",ans); }
八十一:Newest Jaime's Delivery【CF gym】 bit_masking+二分答案+最短路dijkstra验证。
八十二:求长度为n 而且 ai != i 的序列个数 【DP / 容斥 / 错位排列/ 错排】 首先一个序列不对应位置相等,有两种来源:
【1】如果n-1序列不对应位置,那么我在序列末尾加一个,然后拿末尾的数去换,那么一定能得到一个n的不对应序列
比如说: 3 1 2 ,我加一个4 变成 : 3 1 2 4 ,如果4和1,2,3互换的话,就能得到3个合法序列
【2】如果本来那个序列有一个位置是对应的,剩下n-2是不对应的
比如说 3 2 1 中 2是对应的,我加入一个4,4和2换了,3 4 1 2就是符合条件的序列
const ll maxn = 1e6+11,inf = 0x3f3f3f3f3f,mod = 1e9+7; ll dp[maxn],n; inline void solve() { read(n); dp[1] = 0,dp[2] = 1,dp[3] = 2; for(int i = 4;i <= n;i++){ dp[i] = (dp[i-1]*(i-1)%mod + (i-1)*dp[i-2]%mod)%mod; } printf("%lld\n",dp[n]); }
八十三:树上DP【洛谷】或者使用【点分治】
只能说代码很难调。
一般来说,树上的枚举两个节点,然后对答案有贡献,不妨考虑树上DP/点分治
但是如果每个点的贡献次数不一样,那么可能就只能用点分治了,看下面的例题。
//树上DP的细节太多了。。。 const ll maxn = 5e4+11,inf = 0x3f3f3f3f3f,mod = 998244353; ll f[maxn][3],n,m,sm[maxn][3],L,R; vector<PII>e[maxn]; inline void dfs_cnt(int x,int fa){ //定义sm[x][i] 为以x为终点的路径数量,i为长度%3 L += sm[x][0]; for(int i = 0;i < 3;i++) R += sm[x][i]; for(auto [v,l] : e[x]) if(v!=fa)dfs_cnt(v,x); } inline void dfs_pre(int x,int fa){ //由于题目可以存在自边,所以一开始直接赋值为1 f[x][0] = 1; for(auto [v,l] : e[x]){ if(v!=fa){ dfs_pre(v,x); //从子节点那里转移 //由于子节点f[v][0] = 1了,所以不需要f[x][l]++; for(int i = 0;i < 3;i++){ f[x][(i+l+9)%3] += f[v][i]; } } } } inline void dfs_sum(int x,int fa,vector<int>vec){ //定义vec数组为从上往下的路径数量 //定义f为从下往上 for(int i = 0;i < 3;i++) sm[x][i] = f[x][i] + vec[i]; vector<int>tmp(3,0); for(auto [v,l] : e[x]){ if(v!=fa){ //定义:i为到x距离为i的路径数量 //那么tmp里面就要i+L //那么f[v][i-l] 要减去那些以v节点为根的子树边数量 for(int i = 0;i < 3;i++) tmp[(i+l)%3] = sm[x][i] - f[v][(i-l+9)%3]; dfs_sum(v,x,tmp); } } } inline void solve() { read(n); for(int i = 1,t1,t2,t3;i < n;i++){ read(t1,t2,t3); t3 %= 3; e[t1].emp(mp(t2,t3)); e[t2].emp(mp(t1,t3)); } dfs_pre(1,0); dfs_sum(1,0,vector<int>(3,0)); dfs_cnt(1,0); ll g = __gcd(L,R); L /= g,R /= g; printf("%lld/%lld\n",L,R); }
八十四:小Q 与 树 【点分治 / 树链剖分HLD】
点分治+枚举LCA 【如果使用了点分治,可能还要容斥一下,也很快的!】
const ll maxn = 2e5+11,inf = 0x3f3f3f3f3f,mod = 998244353; ll n,a[maxn],ans,sz[maxn],son[maxn]; vector<int>e[maxn]; vector<PLL>vec; bool vis[maxn]; // sz 累计子树的节点数量 // son指的是重儿子的子树节点数【sz最大的儿子--重儿子】 // rt 为重心,重儿子最小的节点【任意一个即可】 vis[0] = son[0] = sz[0] = inf;//!!!!!!初始化操作 inline int root(int x,int fa,const int&tot){ sz[x] = 1,son[x] = 0; int rt=0,tmp=0; for(int v : e[x]){ if(v == fa || vis[v])continue; tmp = root(v,x,tot); sz[x] += sz[v]; // son[x] = max(son[x],sz[v]); // if(son[rt] > son[tmp]) rt = tmp; } son[x] = max(son[x],tot-sz[x]); if(son[x] < son[rt]) rt = x; return rt; } //获取以rt为根的树的节点深度,点权值 inline void get(int x,int dep,int fa){ vec.emp(mp(a[x],dep)); for(int v : e[x]){ if(v == fa || vis[v])continue; get(v,dep+1,x); } } //计算 inline void work(int x,int dep,const ll&sig){ vec.clear(); get(x,dep,0); ll tmp = 0,s1 = 0,s2 = 0; sort(vec.begin(),vec.end()); for(auto [v,d] : vec){ tmp = (tmp + s2 + s1*d%mod)%mod; s1 = (s1+v)%mod; s2 = (s2+d*v)%mod; } ans = ((ans+sig*tmp)%mod+mod)%mod; } inline void dfs(int x){ vis[x] = 1; work(x,0,1);//以x为重心点记录路径数量十分有效 for(int v : e[x]){ if(vis[v])continue; work(v,1,-1); //由于同一颗树的计算还不是最优的,所以减去 dfs(root(v,0,sz[v])); } } inline void solve() { read(n); for(int i = 1;i <= n;i++)read(a[i]),e[i].clear(); for(int i = 1,t1,t2;i < n;i++){ read(t1,t2); e[t1].emp(t2); e[t2].emp(t1); } dfs(root(1,0,n)); printf("%lld\n",ans*2%mod);//答案乘二即可 }
八十六:给出日期,求出n天之后的日期(n可以很大)DATE模板 基础数论
【大佬教程】但我不打算精通蔡勒了。
namespace MY_DATE{ //用于跳转的常量 const string week[8]={ "Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" }; const int year_400 = 1460097; const int m_day[13] ={0,31,28,31,30,31,30,31,31,30,31,30,31}; //辅助函数 bool isLeap(int t){return(t%400==0)||((t%4==0)&&(t%100));} /**year_1[pick(y,m)]*/ int dayThisMonth(int y,int m){return m_day[m]+(m==2&&isLeap(y));} //类的定义 class my_DATE{ public: int year,month,day; my_DATE(int y=2020,int m=1,int d=1):year(y),month(m),day(d){}; bool operator<(const my_DATE&a)const{ return year != a.year ? year < a. year : month != a.month ? month < a.month : day < a.day; } bool operator==(const my_DATE&a)const{ return year == a.year && month == a.month && day == a.day; } bool operator>(const my_DATE&a)const{return !((*this<a)|(*this==a));} my_DATE next_K_Days(int k){ k += this->getCalc(); int x = k + 1789995, n, i, j, y, m, d; n = 4 * x / 146097; x -= (146097 * n + 3) / 4; i = (4000 * (x + 1)) / 1461001; x -= 1461 * i / 4 - 31; j = 80 * x / 2447; d = x - 2447 * j / 80;//计算日期 x = j / 11; m = j + 2 - 12 * x;//计算月份 y = 100 * (n - 49) + i + x;//计算年份 return my_DATE(y,m,d); } int getCalc(){//辅助计算函数,好像是蔡勒公式未化简形式【板子抄的.】 int y = year , m = month , d = day; if(m <= 2) {y--; m += 12;} return 365*y + y/4 - y/100 + y/400 + (153*(m-3)+2)/5 + d - 307; } int daysHasPass()const{ int y = year,m = month; if(y == 0 && m <= 2)return (m==2)*31 + day; if(m <= 2) //如果这一年是闰年,但是没有达到2月,所以不加1 return y*365 + y/4 + y/400 - y/100 + (y>0&&!isLeap(y)) + (m==2)*31 + day; else return y*365 + y/4 + y/400 - y/100 + 60 + //60 = 59 + (y>=0) (int)(13.0*(m-3)/5.0+0.5) + 28*(m-3) + day ; } string Zeller(){//蔡勒公式 int c = year/100 , m = month, y = year , d = day; if(m <= 2){m+=12;y--;}// m 的取值为3~14 y %= 100; // y 取年份后两位 int tmp = (c/4 + y + y/4 - 2*c + (13*(m+1)/5) + d - 1) % 7; //int tmp = (this->getCalc()%7+7)%7 + 1;//等价于zeller,不知道为什么。。 return week[tmp]; } friend int daysBetween(const my_DATE&A,const my_DATE&B){ int f = 0; my_DATE Special(1582,10,4); if(A<Special&&Special<B || B<Special&&Special<A)f = 10; return abs(A.daysHasPass()-B.daysHasPass()) - f; } }; //该命名空间只在1582年10月15号之后的日期有效 }using namespace MY_DATE;
八十七:Complete the MST【最小生成树+并查集+思维+dfs超级减枝】
CF的出题人又教我做人了。
很容易想到设置一条边为异或和,其余为0,但是难的是其他部分啊。。【好吧,难的是那个dfs+并查集】
首先想一个问题,如果没有xor为0的约束,也就是没有设置的边都是 0,那该怎么办?
我们应该看看边权为0的边究竟能不能形成一棵树,否则,就拿已经设置好的边从小到大加进去(没有替换)【类似最小生成树】
① 能不能形成一棵树,只需要看一个节点能不能伸出一条边连到主树就可以了,这一部分使用并查集+dfs维护即可。
但是这个dfs挺难的,n要达到2e5啊,怎么减枝呢?每一次我们都要选一条不存在的边,没有遍历过的节点,然后连边,也就是说,最优情况下每个节点只会被dfs一次,那么压力就降到选一条不存在的边上了。
题解使用了两次二分查找,使用set来存不在树上的节点。
inline void dfs(int x,int fa){ s.erase(x); sort(to[x].begin(),to[x].end()); for(int i = 1;i <= n;i++){ auto it = s.lower_bound(i); if(it==s.end()){ return ; } i = *it; //这一步是减枝的精髓 if(s.count(i) && !binary_search(to[x].begin(),to[x].end(),i)){ A.unite(x,i); tot--;//减去使用的不存在的边 dfs(i,x); } } }
因为set里面的元素一直在减少,但是一次dfs从1~n枚举是不变的,我们不能改变范围,所以我们直接给出一个范围,再二分出一个未被遍历的点,再看看边是不是不存在即可。
之前一直不知道怎么通过不存在的边来访问节点,现在会了。(x
②那么加上限制条件呢?
按照上面的步骤,我们极有可能使用了一条带边权的边(即异或和的边),那么怎么知道有没有用呢?
我们不是 tot-- 了嘛,如果我们 tot 用完了,也就是 tot <= 0,那么就代表我们用的边中,有一条是带边权的,我们现在的任务是揪出它。【这里选用贪心解决】
这条边的条件:不能是pre-assign的,在dfs完毕之后的树上。
题解中使用一个A并查集维护dfs完毕之后的树,然后再慢慢加入pre-assign的边,同时再用一个B并查集维护pre-assign形成的最小生成树,如果一条边的两个节点在同一颗B树上了,那么就没有必要再加进去替换(异或和那条边了,因为之前更小边权的边更有资格)详细再看代码把。【注意:B并查集的作用不能被取代,因为替换的边必须保证是在A中出现,在B中未出现的,”出现“指的是具有相同效果的边】
const int maxn = 2e5+11,inf = 0x3f3f3f3f; ll n,m,ans,mx,sum,tot,ne; vector<array<int,3> >e; vector<int>to[maxn]; set<int>s; struct dsu{ int fa[maxn]; void init(){ for(int i = 1;i <= n;i++) fa[i] = i; } int Find(int x){return fa[x]==x?x:fa[x]=Find(fa[x]);} bool unite(int x,int y){ x = Find(x),y = Find(y); if(x == y)return 0; fa[y] = x;return 1; } }B,A; inline void dfs(int x,int fa){ s.erase(x); sort(to[x].begin(),to[x].end()); for(int i = 1;i <= n;i++){ auto it = s.lower_bound(i); if(it==s.end()){ return ; } i = *it; //这一步是减枝的精髓 if(s.count(i) && !binary_search(to[x].begin(),to[x].end(),i)){ A.unite(x,i); tot--; dfs(i,x); } } } inline void solve() { read(n,m); A.init(),B.init(); for(int i = 1;i <= n;i++) s.insert(i); for(int t1,t2,t3,i = 1;i <= m;i++){ read(t1,t2,t3); e.push_back({t3,t1,t2}); to[t1].emp(t2); to[t2].emp(t1); sum ^= t3; } tot = (n-1)*n/2 - m; sort(e.begin(),e.end()); for(int i = 1;i <= n;i++) if(s.count(i))dfs(i,0); if(tot > 0){ sum = 0; } for(auto [val,x,y] : e){ if(A.unite(x,y)){ ans += val; B.unite(x,y); } else if(B.unite(x,y)){ sum = min(sum,val); } } ans += sum; printf("%lld\n",ans); }
八十八:给定一个数sum,求有多少组序列满足如下条件:
这个序列的元素之和为sum
这个序列的差分数组是递增的
八十九:E - Group Photo【双指针+思维】
这个要想到用双指针来计算真的很难,单调性虽然明显,但是还是不会。。
首先要想出计算方法:枚举L奇偶性不同时的四种情况,然后R指针指向临界的位置,再 (L+R)/2+1计算答案。
一定要先枚举L!!【惨痛教训】
const ll maxn = 2e5+11,inf = 0x3f3f3f3f,mod = 998244353; ll n,a[maxn],ans,sum[maxn],b[maxn],pos; inline ll work(int L,int R,ll ps)//ps :p的值之和 { /* 双指针一定要先枚举L的位置,然后再调整R的位置 这样既不会重复,也不会少算 */ //L :左边连续的C的最右边一个位置 //R :右边连续的P的最左边的C的位置,比如说:CPCPPP //L就是1,R就是3 if(L>R)return 0; if(R%2!=L%2)ps += a[R--]; ps += b[R-1] - b[L-1];//计算出相间的P的和 ll ans = 0; while(L<=R){ while(L<=R&&(ps+a[R])*2<=sum[n])ps+=a[R],R-=2;//等号不能漏掉 if(L<=R&&ps*2<=sum[n]&&(ps+a[R])*2>sum[n])ps+=a[R],R-=2; if(L<=R&&ps*2>sum[n])ans += ((R-L)>>1)+1; ps -= a[L+1]; L += 2; } return ans; } inline void solve() { read(n); ans = 0; for(int i = 1;i <= n;i++){ read(a[i]); sum[i] = sum[i-1] + a[i]; } if(n==1){ return (void)puts("1"); } b[1] = a[1]; for(int i = 2;i <= n;i++){ b[i] = b[i-2] + a[i]; } //暴力计算 for(int i = 1;i <= n;i++){ if(sum[i]*2>sum[n])ans++; } if(n>2){ /* 每一种开头结尾都需要考虑到开头是C,还是CC 也就是奇偶性的两种情况 */ ans += work(1,n-1,a[n]) + work(2,n-1,a[n]);//强制C,P ans += work(2,n-2,a[1]+a[n-1]) + work(3,n-2,a[1]+a[n-1]);//强制P,C ans += work(1,n-2,a[n-1]) + work(2,n-2,a[n-1]);//强制C,C ans += work(2,n-1,a[1]+a[n]) + work(3,n-1,a[1]+a[n]);//强制P,P } else ans += a[n] > a[1];//n=2时,特判一下就好 printf("%lld\n",ans%mod); }
九十:赚钱 【SPFA + 思维】
每个点带有点权,每条边带有负边权,求一条最大权值路径,当出现正权环时输出orz
这个题只需要想到添加一个0节点当作起点,跑最长路就可以了。
- SPFA 的特点在于可以判正/负环,但是会容易被卡时间
- dijkstra适合正权图
- Floyd就适合任意两点之间的最短路
此题仅当n比较小时才能这样做,否则被卡死,还是建议用强连通分量+树上DP来做
思考题:那么要求单向边的图上单源最短环怎么求?把所有单向边分为正向跑一次dijkstra,再把所有边反向跑一次dijkstra即可
九十一:B - Balloon Warehouse 【递归思维】
从一开始插入数字难解决问题,我们来反向思考。
搜先离0最近的数字,一定是最后一组(0,y),然后对于每一个数字都是一样的,所以我们逆向递归。
样例 :0 1 2 1 2 3 ,(0,1)(1,3)(0,1)(1,2)
先从0开始,变成 : 0 1
再从1拓展:0 1 2
因为2后面没有数字了,返回到根节点 1,那么直接填数 0 1 2 3 吗? no。因为这个1出现的时候,3已经出现了,所以这个1与3没有关系,【记录一个 i 索引判断下一个节点的索引是不是大于本节点就行了】,直接返回到0节点。
然后0节点又遍历到(0,1),所以再添一个节点1,变成 0 1 2 1 ,然后这个1的索引又比2小,所以再加一个2,然后返回到1,这个1的索引比3要小,所以加一个3,最后返回到根节点0。
然后除此之外,如果填入的数字不够L大,那么就会出现循环节,取个模就行了【别忘了题目说,ans[0] = 0】
【由于题目说L,R小于1e6,所以数组开到1e6就可以了,复杂度在减枝之后是O(n)的】
const ll maxn = 1e6+11,inf = 0x3f3f3f3f3f,mod = 998244353; int ans[maxn],top,n,L,R; vector<PII>e[maxn]; inline void dfs(int x,int y,int i){ if(top >= maxn || y < 0 || i > e[x][y].se)return ;//减枝,复杂度O(n) ans[top++] = e[x][y].fi; dfs(e[x][y].fi,e[ e[x][y].fi ].size()-1,e[x][y].se); dfs(x,y-1,i); } inline void solve() { read(n,L,R); for(int t1,t2,i = 1;i <= n;i++){ read(t1,t2); e[t1].emp(mp(t2,i)); } top = 1;//因为ans[0] = 0 dfs(0,e[0].size()-1,0); for(int i = L;i < R;i++){ printf("%d ",ans[i%top]); }pln; }
九十二~九十三:树上阶梯NIM博弈
(1)焦糖布丁
根据树上阶梯博弈,我们只要构造出一颗树,使得深度为奇数的节点的异或和为0即可,不妨考虑一颗3层的树,boss为0层,其余为1,2层,那么怎么知道能否构造异或和为0呢?【线性基模板即可】
(2)吃苹果(DIV2 E) 【题解】
根据树上阶梯博弈,但是这一次的奇偶性是对叶子节点来说的。
①如果所有叶子节点的深度为奇数,那么使奇数深度的节点的异或和为0即可。
②如果所有叶子节点的深度为偶数,那么使偶数深度的节点的异或和为0即可。
九十四:勤劳的蜜蜂【关于区域赛出了一道老题这件事】
开一个x,y,z在二维平面的坐标系,然后计算。
我觉得难点主要是建立这个X,Y,Z坐标系之后怎么计算, 不同于曼哈顿,切比雪夫距离,这个坐标系有六种走法, 所以我们要在曼哈顿距离的基础上,再加多一种。 那么怎么才能较快地计算结果呢?一种比较好地方法如下: (1)计算出两点地X,Y,Z,坐标 (2)分别计算abs(dx),abs(dy),abs(dz) (3)然后取三个中两个最小的数加起来就是答案。 最后因为包括起点格子,加一即可。
const ll maxn = 2e5+11,inf = 0x3f3f3f3f,mod = 998244353; ll k; int calc(ll N,vector<ll>&sp,int&Lay){ sp.assign(7,0); ll L = 1,R = inf,mid; while(L < R){ mid = (L+R)>>1; ll tmp = (3LL*(mid-1)*mid); //de(tmp); if(tmp <= N-2)L=mid+1; else R=mid; } mid = (L+R)>>1; mid--; Lay = mid; //mid就是该层数 sp[0] = 3LL*mid*(mid-1)+2; //起点 sp[1] = sp[0] + mid-1; // 左下角点 for(int i = 2;i <= 6;i++) sp[i] = sp[i-1] + mid; for(int i = 1;i <= 6;i++){ if(sp[i] >= N)return i; } return -1; } array<ll,3> get(ll N){ if(N == 1)return { 0 , 0 }; vector<ll>sp; int Lay = 0; int id = calc(N,sp,Lay); assert(id != -1); switch(id){ case 1:return { Lay , sp[1]-N , -Lay+(sp[1]-N) }; case 2:return { sp[2]-N , -Lay+(sp[2]-N) , -Lay }; case 3:return { -Lay+(sp[3]-N) , -Lay , -(sp[3]-N) }; case 4:return { -Lay , -(sp[4]-N) , Lay-(sp[4]-N) }; case 5:return { -(sp[5]-N) , Lay-(sp[5]-N) , Lay }; case 6:return { Lay-(sp[6]-N) , Lay , sp[6]-N }; } throw invalid_argument(" fuck! "); } inline void solve() { read(k); while(k--){ ll L,R; read(L,R); ll ans = 0; auto A = get(L); auto B = get(R); vector<ll>vec{ abs(A[0]-B[0]), abs(A[2]-B[2]), abs(A[1]-B[1]) }; sort(vec.begin(),vec.end()); ans = vec[0] + vec[1]; printf("%lld\n",ans+1); } }
九十五:Alice and Bob【思维】
此题需要一点思维来转换公式。
很容易想到维护一个 f [ i ] ,表示从i开始,到f [ i ] 有k个数,双指针可以O(n)维护。
然后对于一个区间L,R,其对答案有贡献的部分,设为(L,P)
那么这个P的位置 : R[p+1] > R ,这个P下标就二分出来即可
然后答案数就是 : for(L->P) ans += (R - f [ i ] +1);
然后计算一下前缀和,化简公式,就可以O(1)计算了。【什么数据结构都不需要,只需要二分+前缀和】
const ll maxn = 2e5+11,inf = 0x3f3f3f3f3f,mod = 998244353; ll n,m,k,a[maxn],R[maxn],s[maxn]; inline void solve() { read(n,m,k); for(int i = 1;i <= n;i++){ read(a[i]); } a[n+1] = inf;R[n+1]=n+1; map<int,int>mp; for(int i = 1,j = 0;i <= n;i++){ while(mp.size() < k && j <= n) mp[a[++j]]++; if(mp.size() >= k){ R[i] = j; } else R[i] = n+1; mp[a[i]]--; if(mp[a[i]] == 0){ mp.erase(a[i]); } } for(int i = 1;i <= n;i++){ s[i] = s[i-1] + R[i]; } ll ans=0; while(m--){ ll x,y,tx,ty; read(tx,ty); x = min(tx^ans,ty^ans)+1; y = max(tx^ans,ty^ans)+1; ans = 0; //de(x<<" "<<y); int pos = lower_bound(R+1,R+1+n,y+1) - R; if(pos > x){ pos--; ans = (pos-x+1)*(y+1) - s[pos] + s[x-1]; } printf("%lld\n",ans); } }
题意:定义 M*2E 为 0.111111..*21111... ,也就是要使得小数点后M+1位全是1。指数上E位全是1,然后这个数的大小是 A*10B
题目给出A*10B 求M,E的大小。 【该题难在理解题目】
该题只需要一个公式即可:
看到0.11111...有连续多个1,可以想到 1 - 0.5^m,m指的是有多少个连续的1
A*10^B = (1 - pow(1/2,M+1) )*pow(2, pow(2,E)-1 );
这样计算数字太大,超出了long long的范围 ,所以两边取对数,加个精度判断
fabs(log10(pow(2,M+1)-1) - log10(pow(2,M+1)) + (pow(2,E)-1)*log10(2) - log10(A) - B) <= eps; // const long long double eps = 1e-6;
总结:① 二进制下小数点后连续m位数都是1 ,那么这个数就是 1- pow(2,-m);
九十七:Full Depth Morning Show【换根法,树形DP】
九十八:雕塑【三维离散化+种子填充寻找连通块+逆向思维】
再刘汝佳的紫书里面也讲的很清楚,就是三维离散化之后再dfs、bfs即可,但是还是有一些细节优化的,如下:
- 三维离散化尽量使用3个数组,分别存储x,y,z轴,这样可以减少小的格子,减少复杂度【我不会告诉你我一开始使用的是1个离散化数组,结果T了n发】
- (1) 一开始初始化三维网格时,一种很朴素的做法是枚举 i , j , k 看看它在不在某个立方体内部,这样枚举的复杂度是mx*my*mz*n的,mx是x离散化数组的长度,my,mz也是。 (2) 另一种更good的做法是,直接枚举1~n个方格,取左下角的点,然后二分出i , j , k ,然后再有目的性的染色,这样有许多格子就不会被枚举到,所以很快.
const ll maxn = 301,mod = 998244353; const int dx[]={-1,1,0,0,0,0},dy[]={0,0,1,-1,0,0},dz[]={0,0,0,0,-1,1}; typedef vector<vector<vector<int>>> vec_3D; typedef vector<vector<int>> vec_2D; int n,mx,my,mz; vec_3D G; array<int,6>ar[maxn]; vector<int>has[3]; ll S,V; inline void calc(int x,int y,int z,int tp){ switch(tp){ case 0: S+=1LL*(has[1][y+1]-has[1][y])*(has[2][z+1]-has[2][z]);break; case 1: S+=1LL*(has[0][x+1]-has[0][x])*(has[2][z+1]-has[2][z]);break; case 2: S+=1LL*(has[0][x+1]-has[0][x])*(has[1][y+1]-has[1][y]);break; } } inline void dfs(int x,int y,int z){ G[x][y][z] = -1; V += 1LL*(has[0][x+1]-has[0][x])*(has[1][y+1]-has[1][y])* (has[2][z+1]-has[2][z]); for(int i = 0;i < 6;i++){ int tx = x + dx[i]; int ty = y + dy[i]; int tz = z + dz[i]; if(tx>=0&&ty>=0&&tz>=0&&tx<mx-1&&ty<my-1&&tz<mz-1&&G[tx][ty][tz]>=0){ if(G[tx][ty][tz])calc(x,y,z,i/2); else dfs(tx,ty,tz); } } } inline void solve() { S = V = 0; // 初始化 G.assign(maxn,vec_2D(maxn,vector<int>(maxn,0))); read(n); for(int i = 0;i < 3;i++) has[i].clear(),has[i].emp(0); if(n==0){ puts("0 0"); return ; } for(int i = 1;i <= n;i++){ for(int j = 0;j < 6;j++)read(ar[i][j]); for(int j = 0;j < 3;j++){ has[j].emp(ar[i][j]); has[j].emp(ar[i][j]+ar[i][j+3]); has[j].emp(ar[i][j]+ar[i][j+3]+1); } } //三个离散化数组更快 for(int i = 0;i < 3;i++) sort(has[i].begin(),has[i].end()), has[i].erase( unique(has[i].begin(),has[i].end()) , has[i].end()); mx = has[0].size(); my = has[1].size(); mz = has[2].size(); //预处理单元格的性质1/0【初始化的优化】 for(int i = 1;i <= n;i++){ int tx = lower_bound(has[0].begin(),has[0].end(),ar[i][0]) - has[0].begin(); int ty = lower_bound(has[1].begin(),has[1].end(),ar[i][1]) - has[1].begin(); int tz = lower_bound(has[2].begin(),has[2].end(),ar[i][2]) - has[2].begin(); for(int x = tx;x<mx-1&&has[0][x]<ar[i][0]+ar[i][3];x++){ for(int y = ty;y<my-1&&has[1][y]<ar[i][1]+ar[i][4];y++){ for(int z = tz;z<mz-1&&has[2][z]<ar[i][2]+ar[i][5];z++){ G[x][y][z] = 1; } } } } dfs(0,0,0); //计算 V = 1LL*has[0][mx-1]*has[1][my-1]*has[2][mz-1] - V; printf("%lld %lld\n",S,V); }
。。以后多维离散化一定要开多几个数组啊(哭。
九十九: Best Solution unknown【思维+分治+线段树维护】
一道分治的题目,首先要看出最大值这个突破口。
一百:Brilliant Sequence of Umbrellas【思维题】

浙公网安备 33010602011771号