codeforces 基础思维题精选训练(1400 - 2100)
本章节部分参考:2020,2021 年 CF 简单题精选 - 题单 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
首先,很容易观察到点的一些特征:
- 都在第一象限;
- 点的分布越来越稀疏。
以样例为例:

还有无限个点没有画出来。
根据点的分布越来越稀疏的特性,能不能发现收集点的规律呢?
比如我们可以先枚举一个点 \(i\),直接从 \((x_s, y_s)\) 出发去收集 \(\text P_i\)。
然后呢?如果往 \(\text P_0\) 的方向收集,点会非常密集;如果往 \(\text P_\infty\) 的方向收集,点就会非常稀疏。
当然,我们往 \(\text P_0\) 的方向收集!
但是,这边的点是有限的,如果全部收集完了时间还绰绰有余呢?
那就原路返回,再往 \(\text P_\infty\) 的方向收集!
有人可能会疑惑,为什么这里都原路返回了,答案还是最优呢?
首先,因为随着 \(j\) 的增大,\(x_j, y_j\) 都在增大,所以 \(\sum_{j = 1}^{i}\operatorname{dist}(\text P_{j-1}, \text P_j)\)(也就是从 \(\text P_i\) 收集到 \(\text P_0\) 的总距离)就等于 \(\operatorname{dist}(\text P_0 ,\text P_i)\)(\(\operatorname{dist}\) 表示曼哈顿距离)。
下面为了分析方便只看 \(x\) 坐标(\(\operatorname{Xdist}\) 表示 \(x\) 坐标之差)。
点最密集的时候应该是什么时候?很显然,\(a_x\) 和 \(b_x\) 都最小的时候,也就是 \(a_x = 2, b_x = 0\)。
\[ \operatorname{Xdist}(\text P_{i+1}, \text P_{i}) = (a_x \cdot x_{i} + b_x) - x_{i} = (a_x - 1)\cdot x_{i} + b_x = x_i \]
\[ \operatorname{Xdist}(\text P_{0}, \text P_{i}) = x_i - x_0 \]
\(\because x_0 \ge 1 \quad \therefore \operatorname{Xdist}(\text P_{i+1}, \text P_{i}) > \operatorname{Xdist}(\text P_{0}, \text P_{i})\)
现在 \(y\) 坐标也加进来,就可以得到 \(\operatorname{dist}(\text P_{i+1}, \text P_{i}) > \operatorname{dist}(\text P_{0}, \text P_{i})\)。
这说明什么?收集 \(\text P_0 \sim \text P_{i - 1}\) 的时间比只收集一个 \(\text P_{i + 1}\) 的时间还要少!
如果当初选择向右走,那再去收集 \(\text P_{i + 2}\) 的时候,显然 \(\operatorname{dist}(\text P_{i+1}, \text P_{i +2}) > \operatorname{dist}(\text P_{i}, \text P_{i+1})\),那么 \(\operatorname{dist}(\text P_{i+1}, \text P_{i +2}) + \operatorname{dist}(\text P_{i}, \text P_{i+1}) > 2 \operatorname{dist}(\text P_{0}, \text P_{i})\)。说明向 \(\text P_{\infty}\) 方向收集 \(2\) 个点的时候,\(\text P_0\) 方向已经回来了,并收集了 \(i\) 个点,如果 \(i \ge 2\) 那么直接可以知道答案更优了,还剩两种情况:
- \(i=0\),这时没什么左右之分,那不影响答案;
- \(i=1\),直接带入算一算,\(x_1 = 2 x_0\),\(x_2 = 4 x_0\),那么左边加上返回的时间是 \(2 x_0\),直接去 \(\text P_2\) 的时间也是 \(2 x_0\),因为越往后点越稀疏,而两种方案当前耗时相同,起点不同,所以 \(\text P_0\) 方向还是更优。
还有一个小问题,就是数组开多大,因为 \(2^{64} > 10^{18}\),所以数组开到 \(70\) 就绰绰有余了。
时间复杂度 \(\mathcal O(n^2)\),\(n\) 是要用到的点数,算到 \(x_n > x_s, y_n > y_s, \operatorname{dist}(\text P_n, \text S) > t\) 即可。
//https://codeforces.com/contest/1292/problem/B /* 38 70 2 2 67 88 6838924170055088 456766390500883 9176106261147424 调了半天,最终还是A了 由数据范围可知道,由于数据点是以指数级增长的,所以最多有2^64>=10^16,也就是顶多64个 然后 由于数据点一共就64个,不难想到暴力枚举,我认为比较难的地方就在于如何去枚举,如果 我们真的是纯暴力的话,每个点有选或不选,那么就是2^64,肯定不行 由于数据点是指数级爆炸增长,所以说可以这样想,假设原点在某两点之间,那么我们要先把左边的遍历完再遍历右边 可以想 2^0+2^1+....2^n-1相加,他们的和 是不如 2^n大的,所以说我们走2^n这个点不如走n-1所有点 */ //3,144,565,100,727,795 //5,102,364,399,534,485 #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int res; //bool cmp(pair<int,int>a,pair<int,int>b) //{ // return a.first<b.first; //} signed main() { int x0,y0,ax,ay,bx,by,xs,ys,t,f=0,pos; cin>>x0>>y0>>ax>>ay>>bx>>by>>xs>>ys>>t; vector<pair<int,int>>point; point.push_back({x0,y0}); for(int i=1;i<=61;i++){ int x=x0*ax+bx,y=y0*ay+by; if(x<0||y<0) break; if(x0*ax+bx>=1e17||y0*ay+by>=1e17) break; point.push_back({x,y}),x0=x,y0=y; } // sort(point.begin(),point.end(),cmp); // int vis=0; // for(int i=0;i<point.size();i++) // if(point[i]==make_pair(xs,ys)) pos=i,vis++; // // cout<<pos<<endl; // for(auto i:point) cout<<i.first<<' '<<i.second<<endl; // // for(int i=pos;i<point.size();i++){ // int ans=0,nowx=xs,nowy=ys,t_=t; //// cout<<"右: "<<endl; // for(int j=pos+1;j<=i;j++){ //// cout<<t_<<endl; //// cout<<j<<' '<<point[j].first<<' '<<point[j].second<<endl; // if(point[j]==make_pair(xs,ys)) continue; // int x=(abs(point[j].first-nowx)+abs(point[j].second-nowy)); // if(t_>=x) t_-=x,ans++,nowx=point[j].first,nowy=point[j].second; // else break; // } //// cout<<"左: "<<endl; // for(int j=pos;j>=0;j--){ //// cout<<t_<<endl; //// cout<<j<<' '<<point[j].first<<' '<<point[j].second<<' '<<endl; // if(point[j]==make_pair(xs,ys)) continue; // int x=(abs(point[j].first-nowx)+abs(point[j].second-nowy)); // if(t_>=x) t_-=x,ans++,nowx=point[j].first,nowy=point[j].second; // } //// cout<<ans<<endl; // res=max(res,ans); // } for(int i=0;i<point.size();i++){ int t_=t,nowx=xs,nowy=ys,ans=0; for(int j=i;j>=0;j--){ int x=(abs(point[j].first-nowx)+abs(point[j].second-nowy)); if(t_>=x) t_-=x,ans++,nowx=point[j].first,nowy=point[j].second; } for(int j=i+1;j<point.size();j++){ int x=(abs(point[j].first-nowx)+abs(point[j].second-nowy)); if(t_>=x) t_-=x,ans++,nowx=point[j].first,nowy=point[j].second; else break; } res=max(res,ans); } // if(vis>=2) res++; cout<<res; return 0; }
T2:Problem - 1313D - Codeforces
首先注意到 \(k\) 的取值范围,可以想到要用状压。
对于数据范围 \(1 \le m \le 10^9\),显然 \(O(m\, 2^k)\) 的时间复杂度无法接受。
可以发现,\(n\) 个序列中不同的点最多只有 \(2 \times n\) 个,所以进行离散化。
奇偶数之间的转化可以用异或实现。
对于状态 \(s\),可以通过 `__builtin_parity()` 函数快速求得其中有奇数个还是偶数个 \(1\),返回值为 \(1\) 则个数为奇数个,这在 DP 转移时可以用到。
\section*{预处理}
应用差分的思想,对于一个区间 \([ l_i,r_i ]\),将其拆成 \(l_i\) 和 \(r_i+1\),分成开始目前区间和结束目前区间两种操作。
具体实现可以用 STL: \texttt{vector<pair<int,int>>},分别存入 \(( l_i,i )\) 和 \(( r_i+1,-i )\),按序列中位置排序。
\section*{DP}
设 \(f_{i,j}\) 表示离散化后序列第 \(i\) 个位置的状态为 \(j\) 时,第 \(i\) 个位置及其之前的序列中最多的奇数个数,初始值为极小值。
设 \(g_{i,j}\) 表示在序列第 \(i\) 个位置时,第 \(j\) 个对其有影响的区间(没有就是 \(0\))。
- 如果当前操作是开始某个区间
- 遍历数组 \(g_i\),找到第一个 \(0\) 的位置 \(p\),此时 \(g_{i,p}\) 即当前区间是第 \(p\) 个对位置 \(i\) 有影响的区间。
- 如果状态 \(j\) 的第 \(p\) 位是 \(1\),即状态 \(j\) 下加入了当前区间,则 \(f[i][j]\) 应由 \(f[i-1][j^(1<<p)]\) 转移而来。
- 如果状态 \(j\) 的第 \(p\) 位是 \(0\),即状态 \(j\) 下未加入当前区间,则 \(f[i][j]\) 应由 \(f[i-1][j]\) 转移而来。
- 对于开始某个区间的转移,其中都要先记录下离散化后当前位置与下一个位置之间的长度 \(len\)(最后一个位置为 \(0\)),转移时加上 \(len*__builtin_parity(j)\),即当前状态下增加的奇数个数。
- 如果当前操作是结束某个区间
- 遍历数组 \(g_i\),找到当前结束的区间的位置 \(p\) 并清空 \(g_{i,p}\),具体原因可以结合前文理解。
- 如果状态 \(j\) 的第 \(p\) 位是 \(1\),即状态 \(j\) 下依然存在当前区间,可知状态 \(j\) 不合法,所以赋极小值。
- 如果状态 \(j\) 的第 \(p\) 位是 \(0\),即状态 \(j\) 下已不存在当前区间,则 \(f[i][j]\) 应由两个合法状态 \(f[i-1][j]\) 和 \(f[i-1][j^(1<<p)]\) 中的较大值转移而来。
因为当前状态的前一步状态可以本来就没有当前区间即 \(f[i-1][j]\),也可以是在位置 \(i\) 时去掉了当前区间即 \(f[i-1][j^(1<<p)]\)。所以取较大值转移。
- 对于结束某个区间的转移,如果情况合法,也要在转移时加上 \(len*__builtin_parity(j)\)。
\section*{优化}
此时的空间复杂度达到了 \(O(2n2^k)\),所以要对空间进行优化。
- 可以发现,对于开始某个区间的 DP 转移,在 \(f[i][j]\) 由 \(f[i-1][j^(1<<p)]\) 转移而来的情况下, \(j\) 一定大于 \(j^(1<<p)\)。
- 可以发现,对于结束某个区间的 DP 转移,在 \(f[i][j]\) 由 \(f[i-1][j]\) 和 \(f[i-1][j^(1<<p)]\) 中的较大值转移而来的情况下, \(j\) 一定小于 \(j^(1<<p)\)。
所以可以将数组 \(f\) 的第一维省略掉,即设 \(f_i\) 为状态为 \(i\) 时的当前位置前的奇数最大值,并对 DP 的转移略为改动:
- 如果操作是开始区间,就逆向遍历所有状态:`for(int i=(1<<k)-1;i>=0;i--)`,转移时也直接去掉第一维即可。
- 如果操作是结束区间,就正向遍历所有状态,可以类比开始区间的操作。
最后的输出值是 \(f_0\),也就是序列离散化后最后一个位置在不受任何区间影响时的值。
时间复杂度 \(O(n\log_2n+2^k)\),空间复杂度 \(O(2^k)\)。
完结。
/*m数据范围1-1e9,所以复杂度不能带m,如果数据范围较小,完全可以进行差分然后合并 考虑如何突破,注意到,n的范围为1-1e5,并且k最大为8,可以考虑离散化,这里的离散化可以这样做: 对于每个区间[L,R],可以把这段区间变化成[L,i]和[R+1,-i],这样做的意味是,把这些区间排序,然后 如果是正的,那就是要加,如果是负的,那就是减,一直减到遇到正的点,而加或减去的值就是两个相邻点之间的距离 这样我们就有最多2*n个点了,然后考虑,dp[i,j],i为第i位,j为状态,由于k小,显然可以用状态压缩 那么对于正的点开始: 有对于第i位j状态,对它有影响的区间为id,那么如果j状态的id位为1,那么就是 现在这个状态已经包含了id这个区间,那么有dp[i,j]=dp[i-1,j^(1<<id)]+len而来,如果id位为0,那么就是当前状态没有id区间 那么现在这个i位置的状态和i-1位置的状态是一样的,只不过是多加了一个len就是了 对于负的是一样的 考虑id位: 可以这样想: 对于每个点,它最多被8个区间所覆盖,所以我们可以分成8个区间 这道题里,每个位置都只会被最多 8 个线段覆盖,也就是说, 所有线段一定可以无重叠地放置进 8 个轨道。因此只要记录当前位置的所有轨道上是否有线段即可。 现在问题是如何把线段放进轨道里。直接扫描线过去,开一个 vis 数组记录轨道是否占用即可。 之后,第 i 条线段将拥有一个编号 id表示所在轨道的编号。 */ #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; struct node { int x,id,tag; bool operator<(const node&w) const{ if(x!=w.x) return x<w.x; else return tag<w.tag; } }; vector<node>g; int n,m,k,dp[N]; int ID[N]; bool vis[N]; bool check(int x) { return __builtin_popcount(x)%2==1; } signed main() { std::ios::sync_with_stdio(false),cin.tie(0); cin>>n>>m>>k; memset(dp,-0x3f,sizeof dp); for(int i=1;i<=n;i++){ int l,r; cin>>l>>r; g.push_back({l,i,1}),g.push_back({r+1,i,0}); } sort(g.begin(),g.end()),dp[0]=0; for(auto i:g){ if(i.tag) for(int s=0;s<8;s++){ if(!vis[s]){ vis[s]=true,ID[i.id]=s; break; } } else vis[ID[i.id]]=false; } for(int i=0;i<g.size();i++){ node now=g[i]; int id=ID[now.id]; if(now.tag) for(int s=(1<<8)-1;~s;s--){ int v=(i+1<g.size()?(g[i+1].x-g[i].x):0); if(!check(s)) v=0; if((s>>id)&1) dp[s]=dp[s^(1<<id)]+v; else dp[s]=dp[s]+v; } else for(int s=0;s<(1<<8);s++){ if(!((s>>id)&1)){ if(check(s)) dp[s]=max(dp[s],dp[s^(1<<id)])+(i+1<g.size()?(g[i+1].x-g[i].x):0); else dp[s]=max(dp[s],dp[s^(1<<id)]); } else dp[s]=-0x3f3f3f3f; } } cout<<dp[0]; return 0; }
T3:Problem - 1325D - Codeforces
设两个数为 $n$ 和 $m$,我们可以把它们分为 $3$ 部分,有 $x \oplus x \oplus y = m$。对于 $x$ 和 $x$,$x \oplus x = 0$,然后 $0 \oplus y = y$,所以此时让 $y = n$ 即可。
考虑是否有长度为 $2$ 的解,注意由于上一步,我们有 $y = n$,发现,如果有 $(x + n) \oplus x = n$ 或者 $(x \oplus x) \oplus n = n$,那么有 $(x + n) \oplus x = n$ 或 $(x \oplus x) \oplus n = n$,此时答案变成 $2$。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; signed main() { int n,m; cin>>n>>m; int u=m-n; if(u<0||m%2!=n%2) return cout<<-1<<endl,0; if(m==0) return cout<<0<<endl,0; if(n==m) return cout<<1<<endl<<n<<endl,0; int now=u>>1; if(((now+n)^now)==n) cout<<2<<endl<<now<<' '<<now+n<<endl; else if(((now+now)^n)==n) cout<<2<<endl<<now*2<<' '<<n<<endl; else cout<<3<<endl<<now<<' '<<now<<' '<<n<<endl; return 0; }
T4:Problem - 1349B - Codeforces
对于本道题,可以发现,对于一串数组,对于长度为 $2$ 的子串,如果 $i$ 位为 $k$ , $i+1$ 位为 $k+s(s>0)$
那么肯定会有 $i$ 和 $i+1$ 合并变成 $[k,k]$ ,然后依次向后推过去就可以了
所以本题的关键是找到能变的位置,通俗的来讲,就是,只要能变一次,就能够变无数次
这里讲一下我犯的错误,只是着眼于眼前,总以为 $k$ 的时候才能变,其实如果在一个长度为 $3$ 或 $2$ 的区间里
有 $2$ 个数都大于 $k$ ,那么中位数一定大于 $k$,所以这个时候大于 $k$ 的数就出现了,一直把这个数推到 $k$ 旁边即可
我犯的错是只在 $k$ 的时候变化,并没有想到其他数的变化,导致一直 $WA$
//https://codeforces.com/problemset/problem/1349/B /* 对于本道题,可以发现,对于一串数组,对于长度为2的子串,如果i位为k,i+1位为k+s(s>0) 那么肯定会有i和i+1合并变成[k,k],然后依次向后推过去就可以了 所以本题的关键是找到能变的位置,通俗的来讲,就是,只要能变一次,就能够变无数次 这里讲一下我犯的错误,只是着眼于眼前,总以为k的时候才能变,其实如果在一个长度为3或2的区间里 有2个数都大于k,那么中位数一定大于k,所以这个时候大于k的数就出现了,一直把这个数推到k旁边即可 我犯的错是只在k的时候变化,并没有想到其他数的变化,导致一直WA */ //错误代码 #include<bits/stdc++.h> using namespace std; const int N=2e5+10; int a[N],res; bool check(int pos) { if(a[pos]<=a[pos-1]||a[pos]<=a[pos+1]) return true; if(a[pos]>=a[pos+1]&&a[pos]<=a[pos+2]) return true; if(pos>=2&&a[pos]>=a[pos-1]&&a[pos]<=a[pos-2]) return true; return false; } void solve() { int n,k; cin>>n>>k; for(int i=1;i<=n;i++) cin>>a[i]; a[0]=-1,a[n+1]=-1,a[n+2]=-1; if(n==1&&a[1]==k) return cout<<"Yes"<<endl,void(); for(int i=1;i<=n;i++) //只着眼于k这个数,而忘记了其他数 if(a[i]==k&&check(i)) return cout<<"Yes"<<endl,void(); cout<<"NO"<<endl; } int main() { int t; cin>>t; while(t--) solve(); } //正确代码: #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[N],res,n,k; bool check(int pos) { if(a[pos]>=k&&a[pos+1]>=k) return true; if(a[pos]>=k&&a[pos+2]>=k) return true; return false; } void solve() { cin>>n>>k; bool vis=false; for(int i=1;i<=n+2;i++) a[i]=0; for(int i=1;i<=n;i++){ cin>>a[i]; if(a[i]==k) vis=true; } if(n==1&&a[1]==k) return cout<<"Yes"<<endl,void(); if(!vis) return cout<<"No"<<endl,void(); for(int i=1;i<=n;i++) if(check(i)) return cout<<"Yes"<<endl,void(); cout<<"No"<<endl; } signed main() { int t; cin>>t; while(t--) solve(); }
T5:Problem - 1338B - Codeforces
题意
-----
对于每对叶子结点,保证他们之间的最短路径异或和为 $0$,求出现边权种类个数的最大 / 小值。
思路
-----
设一个度不为1的点做跟。
1. 两叶子 $ u, v$ 最短路径的异或和,也就相当于 $u, v$ 到他们根节点的异或和。因为异或具有自反性($LCA$ 异或到根节点,根节点再异或回 $LCA$, 结果不变)。
2. 考虑最小值:如果两个叶子结点的深度和全部为偶数的话,那么答案为 $1$(异或的自反性,偶数个 $a$ 异或结果为 0)。如果出现了深度和为奇数的情况,那么要使得路径上两个节点,那么答案为 $3$(有 $b$ ^ $c$ = $a$,易证 $a, b, c$ 互不相等)。
3. 考虑最大值:将最大值初始化为边数,如果一个节点有 $k$ 个儿子为叶子结点,那么显然他们到父亲的权值必须相同。那么最大值减去 $k - 1$。
时间复杂度 $O(n)$。
#include<bits/stdc++.h> using namespace std; const int N=2e5+10; int n,m,res,idx; int e[N],ne[N],h[N],in[N],dis[N]; bool vis[N],st[5]; void add(int a,int b) { e[idx]=b,ne[idx]=h[a],h[a]=idx++; } int dfs(int u,int fa) { dis[u]=dis[fa]+1; if(in[u]==1) return vis[dis[u]]=true,1; int sum=0; for(int i=h[u];~i;i=ne[i]){ int j=e[i]; if(j!=fa) sum+=dfs(j,u); } res-=max(0,sum-1); return 0; } int main() { cin>>n; res=n-1; memset(h,-1,sizeof h); for(int i=1;i<=n-1;i++){ int u,v; cin>>u>>v; add(u,v),add(v,u),in[u]++,in[v]++; } for(int i=1;i<=n;i++) if(in[i]>1){ dfs(i,0); break; } for(int i=1;i<=n;i++) if(vis[i]) st[i&1]=true; // for(int i=1;i<=n;i++) cout<<dis[i]<<endl; if(st[1]&&st[0]) cout<<3<<' ';//注意这里,这里的dis是根节点到叶子节点的距离,只有一个为奇数一个为偶数的时候相加才是奇数,如果都为奇数那么他俩的距离是偶数 else cout<<1<<' '; cout<<res; return 0; }
T6:Problem - 1396B - Codeforces
又是道思维题,来,让我们仔细分析:
首先规定模型: $t$ 先拿然后 $hl$ 再拿,所以有一个显然的情况:
那就是如果有一堆石头比其他所有的都多,特别多,那么 $t$ 只需要一直呆在这堆石头上就行了,$hl$ 取完所有的也不够 $t$ 自己的这一堆。
然后考虑不满足这种情况的,首先:假设一种为 $4 \ 1 \ 1 \ 1 \ 1$,那么无论 $t$ 他一开始在哪,都不会赢,因为 $t$ 先手。
如果 $t$ 在 $4$,那么 $hl$ 取 $1$,又 $t$ 先手,一定是 $t$ 先取完。如果 $t$ 放弃 $4$ 跑去取 $1$ 呢?那么就变成了 $4 \ 0 \ 1 \ 1 \ 1$,$hl$ 一直守着第一堆就可以了。
如果是 $5 \ 3 \ 1 \ 1 \ 1$,自己推一下又可以发现,无论 $t$ 先手在哪个石堆,经过两次之后,会出现 $5 \ 2 \ 0 \ 1 \ 1$,此时改 $t$ 先手,$t$ 赢。
当然,情况不一定是 $5 \ 2 \ 0 \ 1 \ 1$,但是一定会出现一个很大的堆,因为我们的最大堆和其他的值差值为奇数,也就是第奇数次该 $t$ 选,
所以当出现最大堆的时候,一定是 $t$ 选。
如果某一堆比其他堆加起来还要多,那么先手必胜,可以一直选这一堆。
否则,两人不可能让某一堆比其他堆加起来还要多这种情况出现,否则自己必败,所以所有石子都会被取完,直接判断奇偶性即可。
/* 又是道思维题,来,让我们仔细分析: 首先规定模型: t先拿然后hl再拿,所以有一个显然的情况: 那就是如果有一堆石头比其他所有的都多,特别多,那么t只需要一直呆在这堆石头上就行了,hl取完所有的也不够t自己的这一堆 然后考虑不满足这种情况的,首先: 假设一种为 4 1 1 1 1,那么无论t他一开始在哪,都不会赢,因为,t先手 如果t在4,那么hl取1,又t先手,一定是t先取完,如果t放弃4跑去取1呢?那么就变成了 4 0 1 1 1,hl一直守着第一堆就可以了 如果是 5 3 1 1 1,自己推一下又可以发现,无论t先手在哪个石堆,经过两次之后,会出现 5 2 0 1 1,此时改t先手,t赢 当然,情况不一定是5 2 0 1 1,但是一定会出现一个很大的堆,因为我们的最大堆和其他的值差值为奇数,也就是第奇数次该t选 所以当出现最大堆的时候,一定是t选 */ #include<bits/stdc++.h> using namespace std; const int N=2e5+10; int a[N]; void solve() { int n; cin>>n; int sum=0,maxx=-1; for(int i=1;i<=n;i++) cin>>a[i],sum+=a[i],maxx=max(maxx,a[i]); for(int i=1;i<=n;i++) if(a[i]>sum-a[i]) return cout<<"T"<<endl,void(); if((sum-maxx*2)%2==1) return cout<<"T"<<endl,void(); cout<<"HL"<<endl; } int main() { int t; cin>>t; while(t--) solve(); }
对本题研究较多,写了三种方法:
二分法:
将所有的可能性排序全部放在一个数组里面并记录他们的归属 $id$ ,然后用一个cnt数组进行统计
二分答案,如果说在一个区间l,r内,具有r的值减去l的值符合 $mid$ 约束.并且说这个区间里面包含了n个数,那么就符合check
进行排序是因为l,r的单调性,这样才能进行区间的判断
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[10],b[N],n,cnt[N]; int res=1e18,cur,id; struct node { int id,v; bool operator<(const node&w)const { return v<w.v; } }c[5*N]; bool check(int u) { int num=0; memset(cnt,0,sizeof cnt); for(int l=1,r=1;r<6*n;l++){ while(r<6*n&&c[r].v-c[l].v<=u) num+=((cnt[c[r].id]++)==0),r++; if(num==n) return true; num-=((--cnt[c[l].id])==0); } return false; } void solve() { for(int i=0;i<6;i++) cin>>a[i]; cin>>n; for(int i=1;i<=n;i++){ cin>>b[i]; for(int j=0;j<6;j++) c[i+j*n].v=b[i]-a[j],c[i+j*n].id=i; } sort(c+1,c+6*n+1); int l=0,r=1e10; while(l<r){ int mid=l+r>>1; if(check(mid)) r=mid; else l=mid+1; } cout<<r<<'\n'; } signed main() { std::ios::sync_with_stdio(false),cin.tie(0); int t; t=1; while(t--) solve(); return 0; }
双指针:
与二分的check函数类似,不再赘述,这里注意一点,while循环的时候,双指针的r初始化为0然后先+再更新
这样始终保持r是正确的范围,否则r可能会在id=20是正确的,然后r++导致while结束时为21
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[10],b[N],n,cnt; int res=1e18,cur,id; struct node { int id,v; bool operator<(const node&w)const { return v<w.v; } }c[5*N]; map<int,int>mp; void solve() { for(int i=0;i<6;i++) cin>>a[i]; cin>>n; for(int i=1;i<=n;i++){ cin>>b[i]; for(int j=0;j<6;j++) c[i+j*n].v=b[i]-a[j],c[i+j*n].id=i; } sort(c+1,c+6*n+1); int res=1e15,num=0; for(int l=1,r=0;r<6*n;l++){ while(r<6*n&&num<n) r++,num+=((mp[c[r].id]++)==0); if(num==n) res=min(res,c[r].v-c[l].v); num-=((--mp[c[l].id])==0); } cout<<res<<endl; } signed main() { std::ios::sync_with_stdio(false),cin.tie(0); int t; t=1; while(t--) solve(); return 0; }
堆:
对于一个堆,首先将轨道值从小到大排序,然后先把所有的数都放在第一号轨道里,并且记录最大值
维护一个小根堆,我们每次都从小根堆里面取top元素,然后更新答案,然后我们把这个数放进下一个轨道,也就是pos+1
那么有个显然的性质,如果一个数已经到达最后轨道并且在top中,那么根据top性质,它最小,而后面的数都比它大,而这个数走完了所有轨道
所以此时一定是最优解
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[10],b[N],n,cnt; int res=1e18,cur,id; struct node { int v,id; bool operator<(const node&w)const { return v>w.v; } }c[5*N]; map<int,int>mp; priority_queue<node>que; void solve() { for(int i=1;i<=6;i++) cin>>a[i]; cin>>n; int mx=-1e15; sort(a+1,a+7),reverse(a+1,a+7); for(int i=1;i<=n;i++){ cin>>b[i]; c[i].v=b[i]-a[1],mx=max(mx,c[i].v); que.push({c[i].v,i}),mp[i]=1; } while(!que.empty()){ node now=que.top(); que.pop(); // cout<<now.v<<endl; int i=now.id; res=min(res,mx-now.v); mp[i]++; if(mp[i]>6) return cout<<res<<endl,void(); mx=max(mx,b[i]-a[mp[i]]); que.push({b[i]-a[mp[i]],i}); } cout<<res<<endl; } signed main() { std::ios::sync_with_stdio(false),cin.tie(0); int t; t=1; while(t--) solve(); return 0; }
T8:Problem - 1415D - Codeforces
本题的解法非常的巧妙,观察到,原序列不降,那么对于第 $i$ 个数字
设第 $i$ 个数字为 $x$ ,那么 $x$ 的最高位为 $1$,则有,$i-1$ 个数字关于 $x$ 的最高位也可能是 $1$ , $i+1$ 同理
如果三个连续的数最高位都是$1$,那么就有,$1 \oplus 1=0$,那么最高位抵消,我们只需异或 $i$ 和 $i+1$ 即可,$i-1$ 一定大于他们的异或,一次即可
观察到 $a_i<10^9$,所以一个数的二进制最大位数为 $30$,那么如果我们的 $n>60$,考虑最坏情况,每个位数不可能不出现大于 $3$的情况,所以当 $n>60$ 的时候,操作数一定是$1$,因为最高位为 $1$ 的数一定是相邻的
那么我们的 $n$ 最多为 $60$,直接暴力枚举即可,这样来枚举,考虑两个区间,$[i,j]$, $[j+1,k]$,如果左区间异或完之后大于右区间,那么成立,操作数为 $k-i-1$,可使用异或前缀和
//运用异或前缀和 #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,a[N],s[N],res=1e9; signed main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; if(n>60) return cout<<1<<endl,0; for(int i=1;i<=n;i++) s[i]=s[i-1]^a[i]; for(int i=1;i<=n;i++) for(int j=i;j<=n;j++) for(int k=j+1;k<=n;k++) if((s[j]^s[i-1])>(s[k]^s[j])) res=min(res,k-i-1); cout<<(res==1e9?-1:res)<<endl; }
也可以直接暴力,时间复杂度一样的
//n^3方法: #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,a[N],s[N],res=1e9; signed main() { cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; if(n>60) return cout<<1<<endl,0; for(int i=1;i<n;i++){ for(int j=i;j<n;j++){ int l=0,r=a[j+1],minn=a[j+1],pos=j+1; for(int k=i;k<=j;k++) l^=a[k]; for(int k=j+2;k<=n;k++){ r^=a[k]; if(r<minn) minn=r,pos=k; } if(l>minn) res=min(res,pos-i-1); } } cout<<(res==1e9?-1:res)<<endl; return 0; }
对于本题,总体想法很简单,既然要求奖励是一层一层的变换的,那么不难想到肯定是要把权值大的放在前面,这样才能让他们被计算多次,得到的总奖励也就会越大,所以首先我们要先把数组从大到小排序
对于所有的正数,对答案的贡献是一定的,肯定是能选就选,如果当前我们的打完 $boss$ 的奖励变成了负数,那么我们就要考虑清零次数了,不难看出,对于一串数组来说,我们把最小的数放在最后面是对答案没有影响的
那么对于所有的对答案影响是负的数组,由于我们有 $k$ 次机会,那么可以把这组数组分成 $k+1$ 个集合,由于不一定用完 $k$ 次,所以集合可以为空,那么贪心的去想,我们要对答案的影响最小,所以最小的数一定在后面,那么次小的数呢?倒数第二个数应该是在第二组的最后面才是最优解.尽量的去缩小权值才能得到最优解,所以我们可以得出一个规律: 对于所有对答案负影响的,把他们分为 $k+1$ 组,然后前 $k+1$ 个最小的数组充当 $k+1$ 组的最后面的数,次小的从当倒数第二个,依次类推...
#include<bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10; int a[N],res,sum,n,k; signed main() { scanf("%lld %lld",&n,&k); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); int pos=n; sort(a+1,a+1+n),reverse(a+1,a+1+n); for(int i=1;i<=n;i++){ res+=sum,sum+=a[i]; if(sum<0) {pos=i;break;} } a[pos]=sum; for(int i=pos;i<=n;i++) res+=(n-i)/(k+1)*a[i]; cout<<res<<endl; return 0; }
首先观察函数 $f(i)$,$f(i)$为一个数 $x$ 为 $i$ 的最小公因数,那么有可以转换一下,$f(i)=x$,则有对于 $i$ 来说, $[1,x-1]$ 均可以被 $i$ 整除,这样方向就很明确了,我们要枚举的不是 $i$,而是 $x$,对于每个数,如果他是 $1$ 和 $2$ 的倍数,那么他就对 $1$ 和 $2$ 这两个数有贡献,为 $2$,所以我们枚举 $x=1$ 和$x=2$ 的时候累加一次即可,接下来是如何计算这样一个值,该值满足以下性质: $g[i]$ 为前 $i$ 之前的最小因数,例如 $g[3]=6$ ,意味着 $6$ 可以整除前 $3$ 个数即 $1 2 3$,那么如何计算这个函数呢? 用前 $i$ 个数和当前的 $g[i]$ 求个最小公倍数即可
#include<bits/stdc++.h> #define int long long using namespace std; const int mod=1e9+7; void solve(){ int n,res=0; cin>>n; for(int x=1,i=1;x<=n;x=lcm(x,i),i++) res=(res+n/x)%mod; cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
T11 :Problem - 1548A - Codeforces
不愧是这套题单里面难度最小的,我说怎么我花8分钟就A了
言归正传,题意一开始并不是很清楚,看了样例解释之后才清晰,注意到,对于操作 $3$ 都是围绕这个点的所有邻边删除的,所以不必要建图,既然是最小的那个会被杀死,那么不如开个 $map$ 来储存被杀死的人,然后并且储存这个人的邻边数量,如果操作一或者操作二让 $map$ 变为 $0$ 那么就意味着,他不会被杀死,输出所有人减去应该被杀死的人即可
#include<bits/stdc++.h> using namespace std; const int N=2e5+10; map<int,int>mp; int main() { int n,m,res=0; cin>>n>>m; for(int i=1;i<=m;i++){ int u,v; cin>>u>>v; if(mp[min(u,v)]==0) res++; mp[min(u,v)]++; } int t; cin>>t; while(t--){ int q; cin>>q; if(q==3) cout<<n-res<<endl; if(q==1){ int u,v; cin>>u>>v; if(mp[min(u,v)]==0) res++; mp[min(u,v)]++; } if(q==2){ int u,v; cin>>u>>v; mp[min(u,v)]--; if(mp[min(u,v)]==0) res--; } } return 0; }
T12:Problem - 1490F - Codeforces
最后的答案肯定是所有的数出现的次数都相同,所以我们要进行统一操作,题目还要求最小的操作次数.
这里注意,数组中只能删,不能增,那么就好办了,对于任何一个出现次数较小的数字,那么免除不了被删除的命运,通俗的来说,就是,现在
有两个数,一个出现次数为 $x$ 另一个出现次数为 $y$, 如果我们选择出现次数多的那个(设为 $y$ ),我们就要把 $x$ 全部删除,否则 把 $y$ 删到 $x$,又我们最终的答案一定是一个次数,所以说,对当前的次数进行排序,然后两个两个一组进行比较,看看哪个操作贡献最小,就进行那个操作即可,如果把较大的删除不要忘记小的次数要 $+1$
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[N],n; void solve() { cin>>n; map<int,int>mp,num; for(int i=1;i<=n;i++) cin>>a[i],num[a[i]]++; for(auto it:num) mp[it.second]++; int x_,y_,vis=0; for(auto it:mp){ int x=it.first,y=it.second; if(!vis) x_=x,y_=y,vis=1; else{ if((x-x_)*y>=x_*y_) res+=x_*y_,x_=x,y_=y; else res+=(x-x_)*y,y_+=y; } } cout<<res<<endl; } signed main() { int t; cin>>t; while(t--) solve(); return 0; }
贪心+排序:
首先观察条件,观察到具有这样两个条件: $1: h_1<h_2 , w_1<w_2$ , $2: w_1<h_2 , h_1<w_2$,显然,第二个条件有些别扭,那么对于第二个,它的意思是自己的第二个大于别人的第一个,自己的第一个大于别人的第二个,那么如果我们把所有的 $h$ 和 $w$ 按照 $h>w$ 的放入,也就是当我们的 $w>h$ 的时候,我们就把他俩互换,这样可以消除第二个条件. 为什么这样做是对的? 因为反正我的 $h$ 和 $w$ 在比较的时候可以互换,那么为什么我不一开始就互换呢?性质是一样的,此时只剩下一个条件了,接下来就好办了,按照 $w$ 升序然后判断即可,注意这里相同的 $h$ 要特殊处理
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,res; struct node{ int h,w,id; bool operator<(const node&W) const{ if(h==W.h) return w<W.w; else return h<W.h; } }per[N]; void solve(){ cin>>n; for(int i=1;i<=n;i++){ int a,b; cin>>a>>b; if(a<b) swap(a,b); per[i].h=a,per[i].w=b,per[i].id=i; } sort(per+1,per+1+n); vector<int>res(n+1); int mn=1e9,pos; for(int i=1;i<=n;i++){ int cur=i; while(cur<=n&&per[i].h==per[cur].h){ if(per[cur].w>mn) res[per[cur].id]=pos; else res[per[cur].id]=-1; cur++; } if(mn>per[i].w) mn=per[i].w,pos=per[i].id; i=cur-1; } for(int i=1;i<=n;i++) cout<<res[i]<<' '; cout<<'\n'; } signed main(){ int t; cin>>t; while(t--) solve(); return 0; }
T14:Problem - 1379C - Codeforces
证:因为如果两个物品同时选多次,那么一定是两个都被选过一次,那么用 $b_i$ 更大的去替换另一个一定不劣。
基于贪心的函数递归法:
基于贪心,对于此题来说,有一个母庸置疑的思路就是,我们 $B$ 中的花朵肯定只有一组会被一直选下去,为什么是只选一组呢?其实很好证明: 把所有的 $A$ 和 $B$ 全部混合起来然后从大到小排序,倘若我们在解决的问题中碰到了某一个 $B$,此时有两种选择: 选它(分先手 $a$ 被没被买两种情况),跳过不选,因为如果我们要选这个 $B$,那么我们接下来的花朵价值都没有这个 $B$ 高,所以怎么选都不如这样大.
所以我们的 $B$ 花一定是只有一种被多次选择,而这个 $B$ 是哪个我们很难抉择,因为我们有可能碰到, $A$ 没被先手选,要先选它,但是如果先选它的话会亏损很大一部分从而导致不是正确答案,但是我们知道这样做一定是对的: 前面的全选,到这个 $B$ 之后全部选它,只是我们不知道选哪个 $B$,所以此时我们采用函数递归的方法,交给函数,我们只需要每次递归的时候返回的是 $max$ 值即可了,时间复杂度 $O(nlogn)$,瓶颈在于排序.
// LUOGU_RID: 138973828 #include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; struct node{ int v,id,cur; bool operator<(const node&w) const{ return v>w.v; } }p[2*N]; int tot,num[N][2],n,m; bool vis[N]; int solvep(int now,int cnt){ int res=0; for(int i=now;i<=tot;i++){ if(cnt<=0) return res; if(p[i].cur==0) cnt--,res+=p[i].v,vis[p[i].id]=true; else{ if(vis[p[i].id]) res+=p[i].v*cnt; else{ int val=num[p[i].id][0]+(cnt-1)*p[i].v; res+=max(val,solvep(i+1,cnt)); } return res; } } return res; } void solve(){ cin>>n>>m; tot=0; for(int i=1;i<=m;i++){ cin>>num[i][0]>>num[i][1]; vis[i]=false,p[++tot]={num[i][0],i,0},p[++tot]={num[i][1],i,1}; } sort(p+1,p+1+tot); cout<<solvep(1,n)<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
二分法:
根据前面贪心的证明: 肯定有一个 $B$ 类被选多次,所以说我们可以枚举这个 $b$,然后把大于这个 $b$ 价值的前面所有的 $a$ 都选走,关于的 $a$ 的位置可以用二分来找到,然后通过维护一个前缀和方便实现计入 $a$ 价值,时间复杂度 $O(nlogn)$
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; struct node{ int a,b; bool operator<(const node&w)const{ return a>w.a; } }p[N]; int n,m,sum[N]; void solve(){ cin>>n>>m; for(int i=1;i<=m;i++) cin>>p[i].a>>p[i].b; sort(p+1,p+1+m); for(int i=1;i<=m;i++) sum[i]=sum[i-1]+p[i].a; int res=0; for(int i=1;i<=m;i++){ int l=0,r=m; //注意l要为0: while(l<r){ int mid=(l+r+1)>>1; if(p[mid].a>=p[i].b) l=mid; else r=mid-1; } if(r>=n) res=max(res,sum[n]); else if(r>=i) res=max(res,sum[r]+(n-r)*p[i].b); else res=max(res,sum[r]+p[i].a+(n-r-1)*p[i].b); } cout<<res<<endl; } signed main(){ int t;cin>>t; while(t--) solve(); }
该题比较有弯,首先思考,如果不存在商人的话,我们的基本思路就是每隔 $d$ 个然后吃一个饼干,但是如果碰到商人的话,我们就要重新计数. 首先把没去除商人的情况算出来,具体就是用 $s$ 数组存储商人的位置,然后 $s_0$ 为 $-d+1$,这里解释一下为什么是 $-d+1$,因为我们要统一下计算方式,也就是说从开始到第一个商人所消耗的所有饼干应该是: $(s_1-1-1)/d+1$,这里的 $1$ 是刚开始什么都没有必须吃的 ,然后其中的 $s_1-1-1$ 两次减一,第一次是因为刚开始的第一个距离不算,然后第二次是因为到达第 $s_i$ 处的距离也不算,然后 $s_n+1$段也要特别处理一下即可,这样就算出来不除掉商人的总消耗量,然后计算处理掉商人的情况,对于每个商人,他对答案的贡献无非就是从他的上一个商人开始到他的下一个商人结束的这段情况,所以枚举每个商人,然后把这段特殊情况减去,加上如果没有他应该吃掉的饼干数,然后再加遇到商人吃的数量 $m-1$ ,在循环中更新最小值和答案即可.
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,m,t,d; void solve(){ cin>>n>>m>>d; vector<int>s(m+2); for(int i=1;i<=m;i++) cin>>s[i]; s[0]=-d+1,s[m+1]=n+1; int sum=0,res=n+1,cnt=1; for(int i=1;i<=m+1;i++) sum+=(s[i]-s[i-1]-1)/d; for(int i=1;i<=m;i++){ int ans=sum; ans-=(s[i]-s[i-1]-1)/d,ans-=(s[i+1]-s[i]-1)/d,ans+=(s[i+1]-s[i-1]-1)/d; ans+=m-1; if(ans<res) res=ans,cnt=1; else if(res==ans) cnt++; } cout<<res<<' '<<cnt<<endl; } signed main(){ cin>>t; while(t--) solve(); }
#include<bits/stdc++.h> #define int long long signed main(){ int n,m,k,l,res; std::cin>>n>>m>>k>>l; if(m>n||n-k<l) return std::cout<<-1,0; res=((k+l)%m)==0?(k+l)/m:((k+l)/m)+1; std::cout<<(res*m>n?-1:res); }
T17:Problem - 804B - Codeforces
#include<bits/stdc++.h> #define int long long using namespace std; const int mod=1e9+7; signed main(){ string s; cin>>s; int res=0,cur=0; for(int i=s.size()-1;i>=0;i--){ if(s[i]=='b') cur=(cur+1)%mod; else res=(res+cur)%mod,cur=(cur*2)%mod; } cout<<res; }
T18:Problem - 1491C - Codeforces
贪心的去想,对于每一张床,我们都需要把他变成1,那么我们从一个必须跳的地方开始一定是最优的,因为如果我们从其它床开始能跳到这个地方的,显然前面那个床更优,所以从左向右第一个非 $1$ 的床是起点,然后记录差分,计算出每个床的贡献,如果当前的跳过次数 $h$ 小于当前的值的话,那么就意味着在某次之后要从这个床为起点,更新答案然后记录差分,对于已经变成 $1$ 的,那么他前面的那个床也会有次数贡献,不要忘记更新记录
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[N],c[N]; void solve(){ int n; cin>>n; for(int i=1;i<=n;i++) cin>>a[i],c[i]=0; int h=0,res=0; for(int i=1;i<=n;i++){ h+=c[i]; if(h>a[i]-1){ c[i+1]+=h-a[i]+1,c[i+2]-=h-a[i]+1; c[i+2]++,c[min(n+1,i+a[i]+1)]--; } else res+=a[i]-1-h,c[i+2]++,c[min(n+1,i+a[i]+1)]--; } cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); return 0; }
T19:Problem - 1490G - Codeforces
寒假告一段落,我的寒假还有仅存的 $20$ 多天,在保持作业的基础下也是又重新开始了我的50题精选系列,回到本题:
对于这种无限长的序列来说,我的第一想法是,算出它的圈数,也就是加上一圈之后我能增加多少,然后整除取余数即可,但是很快意识到不对劲,你会发现如果到 $x-1$ 圈之后,前 $i$ 个数的前缀和要比一圈的大很多,所以我们要时刻记录一圈中的区间最大值,接下来就是二分圈数,用 $mx$ 数组记录到某个点的时候区间的最大值,如果当前二分的圈数 $s$ 有: $s*sum[n]+mx[n] >= q$ 的话,那么很明显我们就只进行了 $s$ 圈,剩下的在 $1$ 圈里找即可,这里注意一些特判情况,具体看代码:
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,m,a[N],mx[N],sum[N]; bool check(int u,int q){ return u*sum[n]+mx[n]>=q; } void solve(){ cin>>n>>m; mx[0]=-1e18; for(int i=1;i<=n;i++){ cin>>a[i]; sum[i]=sum[i-1]+a[i],mx[i]=max(mx[i-1],sum[i]); } vector<int>res; while(m--){ int q; cin>>q; if(mx[n]>=q) res.push_back((lower_bound(mx+1,mx+1+n,q)-mx-1)); else if(sum[n]<=0) res.push_back(-1); else{ int l=0,r=1e9; while(l<r){ int mid=l+r>>1; if(check(mid,q)) r=mid; else l=mid+1; } res.push_back(r*n+(lower_bound(mx+1,mx+1+n,q-sum[n]*r)-mx)-1); } } for(auto x: res) cout<<x<<' '; cout<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
T20: Problem - 1470A - Codeforces
这里题目简化了,给定的 $c$ 数组是递增的,那么我们只需要对 $A$ 进行排序,然后倒序求即可,利用 $Min$ 函数计算当前应该是送钱好还是买礼物更好
如果 $c$ 数组不是递增的,可以开一个 $mn$ 数组记录时刻的区间最小值,然后利用此进行判断即可
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; int n,m,a[N],c[N],res; bool cmp(int x,int y){ return x>y; } void solve(){ cin>>n>>m; res=0; for(int i=1;i<=n;i++) scanf("%d", &a[i]); for(int i=1;i<=m;i++) scanf("%d", &c[i]); sort(a+1,a+1+n,cmp); for(int i=1;i<=n;i++) res+=min(c[min(m,i)],c[a[i]]); cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
由于 $n$ 的最大上限为 $1e19$, 故很明显暴力不可以,考虑二进制,试想一下二进制的最低位,由于 $0-n$ 中,其一定是奇偶奇偶的变化,也就是最低位为 $0->1->0$ 的变化,这样算做一次贡献,那么用这个思想考虑倒数第二位,倒数第二位为: $0->0->1->1->0->0$ 的变化,也就是两次贡献加一,依次类推即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; string s; int n,t,a[N],f[N],res,num,ans,m,k; bool vis[N]; void solve() { cin>>n; int res=0; while(n>0) res+=n,n>>=1; cout<<res<<endl; } signed main() { std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); cin>>t; while(t--){ solve(); } return 0; }
首先考虑清零之后的情况,由于是前 $1~b_j$ 个数自增,显然我们得到的序列一定是一个单调不减的序列,那么它的最大的贡献值为 $1$,所以当我们第一次清零之后,往后只需要重复自增清零这两次操作即可
接下来考虑什么时候进行第一次清零,考虑枚举次数,这里枚举 $2*n$ 次,为什么是 $2*n$ 次? 因为我们在清零之后两次增加一个贡献,在未清零之前,我们最多具有的贡献是 $n$ 那么这个贡献和我们做 $2*n$ 次清零自增的贡献是一样的,所以往后枚举就没有意义了
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,m,d,a[N],b[N],c[N]; void solve(){ cin>>n>>m>>d; int res=0; for(int i=1;i<=n;i++) cin>>a[i]; for(int i=1;i<=max(2*n,m);i++){ if(i<=m) cin>>b[i]; else b[i]=b[i-m]; } for(int i=0;i<=2*n+1;i++){ int ans=0; for(int j=1;j<=b[i];j++) a[j]+=1; for(int j=1;j<=n;j++) if(a[j]==j) ans++; if(i+1<=d) res=max(res,ans+(d-i-1)/2); } cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
观察性质,发现一个性质,当 $i>=3$ 时,对于所有我们想取的数,我们都可以取到,因为当我们想取的数在偶数位,我们只需要删除前面一个偶数位的负数即可了,因为我们前面的正数都取完了,所以一定有偶数位的负数,然后特殊判断一下 $i=1和i=2$ 的情况即可
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,m,res,a[N]; void solve(){ cin>>n; res=0; for(int i=1;i<=n;i++){ cin>>a[i]; if(i>=3&&a[i]>=0) res+=a[i]; } if(n==1) cout<<max((int)0,a[1])<<endl; else cout<<res+max({(int)0,a[1],a[1]+a[2]})<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
考虑当我们每切下去一块会产生的贡献,如果我们在序列 $[1,2,3,4,5,6]$ 中,在$3$号位切下去,那么会变成$[1,2][3,4,5,6]$,也就是我们多产生了$[3,4,5,6]$这个和的贡献所以,考虑维护一个后缀和或者前缀和,当$n--i$的区间和大于0的时候我们就可以切一块
做这类题不要想着去乘,而是分成一块一块的加上去,大多数是这种的思路
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int a[N],s[N],n,m,res; void solve(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i],s[i]=s[i-1]+a[i]; int res=0; for(int i=1;i<=n;i++) if(i==1||s[n]-s[i-1]>=0) res+=s[n]-s[i-1]; cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
这次带来一个 $1200$ 的构造题,这个构造题与往常的不太一样.
对于矩阵 M 考虑,我们设数组 $res$ 中的 $a_i 和 a_j$ 一开始为无穷大,也就是二进制全是 $1$,然后通过与运算,我们尽量的保住 $1$,构造完之后,再去检查一遍是否符合矩阵即可
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2040; int n,mp[N][N],res,a[N]; void solve(){ cin>>n; if(n==1){ cin>>mp[0][0]; cout<<"YES"<<endl<<0<<endl; return; } vector<int>res(n+2); for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) cin>>mp[i][j],res[i]=0x7ffffffff; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(i!=j) res[i]&=mp[i][j]; for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) if(i!=j&&(res[i]|res[j])!=mp[i][j]) return cout<<"No"<<endl,void(); cout<<"YEs"<<endl; for(int i=1;i<=n;i++) cout<<res[i]<<' '; cout<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
考虑构造,对于第 $i$ 列的矩阵,我们考虑让其$MEX_i = i-1$,然后考虑 $n和m$ 的关系,如果 $m>n$ 的话,则答案为 $n+1$,因为 $n$ 行最多限制到 $n$,此时在矩阵的最左边制造一个符合 $0-n$ 的矩阵然后向右填补即可,如果$n>=m$ 的话,同理,不过这次填补的矩阵在下方
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,m,res; void solve(){ cin>>n>>m; if(m==1){ for(int i=0;i<=n;i++) cout<<0<<endl; return; } else if(m>n){ cout<<n+1<<endl; for(int i=1;i<=n;i++){ for(int j=1;j<=n+1;j++) cout<<(i+j-1)%(n+1)<<' '; for(int j=n+2;j<=m;j++) cout<<j-1<<' '; cout<<endl; } } else{ cout<<m<<endl; int x=n,n=m-1; for(int i=1;i<=n;i++){ for(int j=1;j<=n+1;j++) cout<<(i+j-1)%(n+1)<<' '; cout<<endl; } for(int i=n+1;i<=x;i++){ for(int j=1;j<=m;j++) cout<<j%m<<' '; cout<<endl; } } } signed main(){ int t; cin>>t; while(t--) solve(); }
首先考虑 $k>=n$ 那么我们肯定会遍历一遍序列,为了损失最小化,我们把遍历序列的次数留到最后 $n$ 次即可
然后考虑 $k<n$ 这时候考虑区间长度为 $k$的最大值即可,不需要停留,因为当我们走一步的时候,前面或后面的个数必定大于等于$1$,我们不动获得的是 $1$ 的贡献,因此向前走不回头肯定是最优方案
#include<bits/stdc++.h> #define int long long const int N=2e5+10; using namespace std; int n,k,a[N],s[N],ss[N]; void solve(){ cin>>n>>k; int res=0; for(int i=1;i<=n;i++) cin>>a[i]; if(k>=n){ int now=k-n; for(int i=1;i<=n;i++) res+=a[i]+now,now++; } else{ for(int i=1;i<=n;i++) s[i]=s[i-1]+a[i]; for(int i=1;i<=n;i++) res=max(res,s[min(n,i+k-1)]-s[i-1]+k*(k-1)/2); } cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
首先正序遍历肯定是不可取的,因为无法确定后面最小的数是什么,所以没办法分数,故要从后向前推,考虑每一位 $a_i$,对于数组从后向前,维护一个最小数 $num$,若 $a_i<=num$ 则更新 $num$,否则我们需要切分,考虑如何切分,我们需要使切分次数尽可能的小,还希望我们的最小值$num$尽可能地大,故考虑均分,对$a_i$平均分块,这样我们的就能最大化我们的最小值,当最小值大的时候,切分次数自然会小,故这样贪心是正确的
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e6+10; int n,a[N]; void solve(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; int num=a[n],res=0; for(int i=n-1;i>=1;i--){ if(a[i]<=num) num=a[i]; else{ int k=(a[i]%num==0?a[i]/num:a[i]/num+1); num=min(num,a[i]/k),res+=k-1; } } cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
T29:Problem - 1905C - Codeforces
观察题目要求,可以得出我们首先要找到最大的那个字符下手,因为我们每次操作都选择字典序最大的那个子串,所以很明显的若最大的字符一开始就在串尾,则无论如何操作都没办法改变字符串,所以我们此时有一个明显的想法:
找出以最大字符开头的最长不降序列,然后将该序列翻转,最后遍历字符串检查是否不降,长度即为最长不降序列减一即可
有了这个思想,可以继续向下考虑,对于任何满足最后不降的字符串来说,从串尾到最大字符位置,以不降的性质去组成一个不降的序列,这个序列即为目标序列,因为若反转之后不满足条件,就证明从后向前的过程中有遗漏的小字符,但是如果把这个小字符加上,后面的字符无法拼接,一样无法满足,即我们最后的目标串一定是满足这个性质的,把这个串翻转之后检查即可
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n; struct node{ char c; int pos; }s[N],now[N]; bool vis[N]; void solve(){ cin>>n; char maxx='a'-1; int max_pos,num=0,m=1; memset(vis,false,sizeof vis); for(int i=1;i<=n;i++){ char c; cin>>c; s[i]={c,i}; if(c>maxx) max_pos=i,maxx=c,m=1; else if(c==maxx) m++; //可能出现多个最大字符并联,最后减的时候要减去 } char c='a'; for(int i=n;i>=max_pos;i--) if(s[i].c>=c) now[++num]=s[i],c=s[i].c,vis[i]=true; num=0; for(int i=max_pos;i<=n;i++) if(vis[i]) s[i].c=now[++num].c; for(int i=2;i<=n;i++) if(s[i].c<s[i-1].c) return cout<<-1<<endl,void(); cout<<num-m<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
T30:Problem - 1919C - Codeforces
观察题目要求,本意为将序列在不改变前后顺序的情况下分成两个子序列并计算最小贡献,那么考虑如何分,很明显我们要维护两个序列,设$x,y$为两个序列的末尾数字且 $x>=y$ 意味这第一个序列的值基本上都大于第二个序列,考虑$a_i$应该放在哪里
$a_i > x$:这意味着无论放在哪个子序列都会导致答案多一,但是显然将 $y \gets a_i$ 更优。因为这会使 $x, y$ 增大的尽量多。
$a_i > y$:如果 $y \gets a_i$,那么答案会多一,显然不优。因此 $x \gets a_i$。
$a_i \le y$:显然此时放在哪个子序列中答案都不会加一。但是这样修改后一定会让它们变小,所以我们选择 $y \gets a_i$,即让损失较小。
#include<bits/stdc++.h> #define int long long using namespace std; const int N=2e5+10; int n,a[N]; void solve(){ cin>>n; for(int i=1;i<=n;i++) cin>>a[i]; int x=1e19,y=1e19,res=0; for(int i=1;i<=n;i++){ if(x<y) swap(x,y); if(a[i]>x) y=a[i],res++; else if(a[i]>y) x=a[i]; else if(a[i]<=y) y=a[i]; } cout<<res<<endl; } signed main(){ int t; cin>>t; while(t--) solve(); }
首先考虑一个合法的字符串应该满足什么样子,对于 $i$ 位置来说,它可以是任何字母,也就是说,合法的字符串必须具备一个这样的能力可以使$s_i$为任何字母,只有这样才能全部满足所有的可能情况,所以我们的一个理想串在 $n=3,k=3$的情况下应该是: $abcd,abcd,abcd$,只有这样我们才能满足这样的条件,中间的字符随便加,所以我们只需要找出满足这种的就可以了
接下来考虑不合法情况,应该如何输出,考虑在遍历的时候记录不存在的字符,例如 $abcd$ 中 $c$ 没出现,所以我们只需要加一个 $c$的贡献即可,如果符合某一遍的遍历结果,那么就随便增加一个贡献即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n,k,m; cin>>n>>k>>m; string s,res=""; cin>>s; int j=0,ok=1; for(int i=1;i<=n;i++){ vector<char>vis(30); int cnt=0; while(cnt<k&&j<m){ cnt+=!vis[s[j]-'a']; vis[s[j]-'a']=1,j++; } if(cnt<k){ ok=0; for(char x='a';x<'a'+k;x++) if(!vis[x-'a']){ res+=x; break; } } else res+=s[j-1]; } if(ok) cout<<"Yes"<<endl; else cout<<"No"<<endl<<res<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
本题虽然是 $1400$,但感觉第一思路没想对的话,后面难度就挺大的,首先说下我的思路:
首先我的思路是考虑先用 $1$ 的注输 x 场,然后 $all in$ 赢的,然后计算是否赚钱,但是很快发现输赢的操作不是我能操纵的,而且下注的钱也要自己决定,于是放弃,后面跟着 $jiangly$ 又以为是 $dp$ 随后发现不满足规划性质,那么现在考虑如何进行必赢的操作:
我们肯定是不能受对方的控制,所以仅存的一个必赢的操作为: 我赢一场之后我会把前面输的钱都赚回来并且比前面赚的多
形式化的说: 设 $0~x$场次,那么有$y_0 y_1 y_2...y_x$ 满足一个性质:
$\forall i, \quad \forall j < i, \quad y_i > \sum_{j=1}^{i-1} y_j$
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int k,x,n; cin>>k>>x>>n; int sum=0; for(int i=0;i<=x;i++){ int y=sum/(k-1)+1; sum+=y; if(sum>n) return cout<<"NO"<<endl,void(); } cout<<"YES"<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
题意比较难理解,这里通俗的讲一下,就是对于一张简单图来说,如果说有一个集合: ${1,2,3,4}$,如果从 $1到4$ 的简单路径中包含了 $2或3$,那么该集合不合法,求所有的合法集合的方案数
对于该题,考虑动态规划,对于每一个根节点,如果说子树中有一个被选中了,如果这个根节点也被选中了,那么可以得到除了这个子树之外的所有子树都不能再选,因为下面的点想要上去必定会经过根节点,定义这种树为状态 $2$,那么相反的如果只有根节点或者下面的节点的话,是可以在其它子树外面选点的,所以这种为状态$1$
对于状态一,很明显它可以选择根节点,或者不选根节点选择子树也就是 $1+dp[v_1][0]*dp[v_2][0]*...$
对于状态二,可以确定的是要选择根节点,然后考虑子树,子树中肯定要从好的子树中选择一个节点,所以这个节点必须确保,只有这一个子树有并且同时其它状态为 $1$ 的子树均不影响,也就是 $dp[v_1][0]-1+dp[v_2]-1+...$
最后别忘记只选根节点不选子树的情况也就是每个节点答案加$1$
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=998244353; vector<int>g[N]; int dp[N][2]; void dfs(int u,int fa){ dp[u][0]=1,dp[u][1]=0; for(auto x:g[u]){ if(x==fa) continue; dfs(x,u); dp[u][0]=(dp[u][0]%mod*dp[x][0]%mod)%mod; dp[u][1]=(dp[u][1]+dp[x][0]-1)%mod; } dp[u][0]++; } void solve(){ int n; cin>>n; for(int i=0;i<=n+1;i++) g[i].clear(),dp[i][0]=dp[i][1]=0; for(int i=1;i<n;i++){ int u,v; cin>>u>>v; g[u].push_back(v),g[v].push_back(u); } dfs(1,0); int res=dp[1][0]; for(int i=1;i<=n;i++) res=(res+dp[i][1])%mod; cout<<res%mod<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
对于这道题来说,我的第一想法是找到所有的环,明显的,只要是与环相连的点一定是合法的,所以说如果存在一个环,那么这个环上的点和与这些点相连的点的贡献均为 $0$,第一想法是枚举环,后来在遍历的过程中发现,只需要对所有的点做一遍 $dfs$ 即可,只要是遍历过的点就不用遍历了,如果遍历了一个没有环的图,那么起始相连,一定会有一个点不合法,故这个块的贡献加一即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; int res; bool vis[N],f; vector<int>g[N]; void dfs(int u,int fa){ if(vis[u]) return f=true,void(); vis[u]=true; for(auto x:g[u]) if(x!=fa) dfs(x,u); } signed main() { std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); int n,m; cin>>n>>m; while(m--){ int a,b; cin>>a>>b; g[a].push_back(b),g[b].push_back(a); } for(int i=1;i<=n;i++){ if(!vis[i]){ f=false,dfs(i,-1); if(!f) res++; } } cout<<res; return 0; }
是一个大典题,已经给你了跳跃区间,求正好到达终点的最小贡献和,很明显的单调队列优化dp,维护一个双端队列,用单调队列维护 $dp$的最小值,窗口为跳跃区间即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n,m,k,d,res=1e18; cin>>n>>m>>k>>d; d++; vector<int>num(n+1); for(int i=1;i<=n;i++){ vector<int>a(m+1),dp(m+1,1e18); deque<int>que; dp[1]=1; for(int j=1;j<=m;j++) cin>>a[j],a[j]++; for(int j=2;j<=m;j++){ while(!que.empty()&&dp[j-1]<dp[que.back()]) que.pop_back(); que.push_back(j-1); while(que.front()<j-d) que.pop_front(); dp[j]=a[j]+dp[que.front()]; } num[i]=num[i-1]+dp[m]; } for(int i=k;i<=n;i++) res=min(res,num[i]-num[i-k]); cout<<res<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
最痛苦的一集,题意是这样的,给你一个有向无环图,然后给一个相邻两点一定有一条边相连的序列,让你简化这个序列使得按照新的序列走仍然能走出原序列,要按照最短路走,对于该题,由于点数不大于 $100$,故考虑 $Floyd$ 把各个点的最短路都求出来,然后开始筛.首先一定有 $p_i和p_i+1$相连,所以我们设一个起始点$now$,然后遍历原序列,当遍历到 $pos$的时候,这时候路的长度一定是 $pos-now$,如果比这个小,那么一定有起始点与这个点相连,所以我们要把上一个点加进去,以确保我们走的是这条路而不走从起始点到这个点的路,然后重置起始点为 $pos-1$即可
#include<bits/stdc++.h> using namespace std; const int N=1e6+10; int mp[105][105]; void solve(){ int n; cin>>n; memset(mp,0x3f,sizeof mp); for(int i=1;i<=n;i++){ for(int j=1;j<=n;j++){ int x; scanf("%1d",&x); if(x) mp[i][j]=1; } mp[i][i]=0; } for(int k=1;k<=n;k++) for(int i=1;i<=n;i++) for(int j=1;j<=n;j++) mp[i][j]=min(mp[i][j],mp[i][k]+mp[k][j]); int m,now=1; cin>>m; vector<int>res,num(m+1); for(int i=1;i<=m;i++) cin>>num[i]; for(int i=1;i<=m;i++){ if(i==1) res.push_back(num[i]); if(mp[num[now]][num[i]]<i-now) now=i-1,res.push_back(num[i-1]); if(i==m) res.push_back(num[m]); } cout<<res.size()<<endl; for(auto x:res) cout<<x<<' '; } signed main(){ int t; t=1; while(t--) solve(); }
可以添加也可以不添加,如果添加之后还不如原来好那还不如不加,这是一点. 然后考虑添加之后的答案,由于我们只能添加一个数,所以肯定要把最大的那个最小,找到位置之后,考虑一定是$(a_{\text{pos}} - a_{\text{pos}-1})/2$ 所以我们直接找最靠近这个数的数即可,遍历$D$数组从$F$数组二分找即可最后比较一下答案输出即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; int a[N],d[N],f[N]; void solve(){ int n,m,k,num=-1,pos,res=1e18,vis=1,maxx=-1; cin>>n>>m>>k; for(int i=0;i<=max({n,m,k})+10;i++) a[i]=f[i]=d[i]=1e18; for(int i=1;i<=n;i++){ cin>>a[i]; if(i>1&&a[i]-a[i-1]>num) num=a[i]-a[i-1],pos=i,maxx=num; } for(int i=1;i<=m;i++) cin>>d[i]; for(int i=1;i<=k;i++) cin>>f[i]; sort(d+1,d+1+m),sort(f+1,f+1+k); int num1=a[pos-1]+num/2,num2=a[pos-1]+num/2+((num%2)?1:0); for(int i=1;i<=m;i++){ int x=lower_bound(f+1,f+1+k,num1-d[i])-f; int y=lower_bound(f+1,f+1+k,num2-d[i])-f; res=min(res,max(abs(d[i]+f[x]-a[pos-1]),abs(a[pos]-(d[i]+f[x])))); res=min(res,max(abs(d[i]+f[x-1]-a[pos-1]),abs(a[pos]-(d[i]+f[x-1])))); res=min(res,max(abs(d[i]+f[y]-a[pos-1]),abs(a[pos]-(d[i]+f[y])))); res=min(res,max(abs(d[i]+f[y-1]-a[pos-1]),abs(a[pos]-(d[i]+f[y-1])))); } int ans=-1; for(int i=2;i<=n;i++){ if(i==pos) continue; ans=max(ans,a[i]-a[i-1]); } cout<<min(maxx,max(ans,res))<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
考虑贪心,既然需要压缩我们的树的高度,那么就尽可能地去在一定高度内增加节点,所以说一定是先放有两个子节点的节点,然后再放一个的方法一定是最优的,因为这样我们才能扩大树的宽度,减少树的高度. 对于有两个节点的节点,明显的他对节点数的贡献是 $+1$,而一个子节点的贡献是 $0$,所以说我们先按照二叉树的方式构建,然后得到的叶节点的数目是一定的,如果 $c$ 不等于这个数目那就无解,然后有一个子节点的节点依次放入二叉树即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=3e5+10,mod=1e9+7; int lg[N]; void solve(){ int a,b,c; cin>>a>>b>>c; if(!a){ if(c!=1) return cout<<-1<<endl,void(); else return cout<<b<<endl,void(); } int cnt=1,a_=1,tmp=a,high=0; while(tmp>cnt) tmp-=cnt,cnt*=2,high++; cnt*=2,a_=max(tmp*2+(cnt-tmp*2)/2,a_); if(c!=a_) return cout<<-1<<endl,void(); if(tmp) high++,b-=(cnt-tmp*2)/2; while(b>=a_) high++,b-=a_; cout<<((b>0)?high+1:high)<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
T39: codeforces.com/contest/1950/problem/G
一看 $n$ 的数据过于小,首先想到压状,设定$k$个状态,即$1<<n$,对于每个状态的二进制中的$1$就代表着选择这首歌曲,所以我们直接进行遍历,设$dp[k][i]$以歌曲$i$结尾是否可以,初始化之后遍历所有状态,对于每个状态,我们都更新一遍答案,然后对于第$i$首歌曲,如果可以与第$j$首歌曲相连,那么我们更新一下状态,也就是以$j$结尾,然后这个状态的第$j$位为真即可
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n; cin>>n; vector<string>s(n),g(n); for(int i=0;i<n;i++) cin>>s[i]>>g[i]; vector<vector<bool>>vis(n,vector<bool>(n,false)); for(int i=0;i<n;i++) for(int j=0;j<n;j++) vis[i][j]=(s[i]==s[j]||g[i]==g[j]); vector<vector<bool>>dp(1<<n,vector<bool>(n,false)); for(int i=0;i<n;i++) dp[1<<i][i]=true; int res=0; for(int k=1;k<(1<<n);k++){ for(int i=0;i<n;i++){ if(dp[k][i]){ res=max(res,(int)__builtin_popcount(k)); for(int j=0;j<n;j++) if(!(k&(1<<j))&&vis[i][j]) dp[k|(1<<j)][j]=true; } } } cout<<n-res<<endl; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
题意不在赘述,首先对于一段区间 $[l,r]$,由于区间前缀异或和的性质,那么如果我们的区间异或和为 $0$,那么我们一定可以随便分出两段,然后一定有$[l,x]$$\oplus$$[x,r]$为$0$,也就是这两段异或和相同
接下来考虑区间异或不相同的情况,那么我们一定可以把这段区间分成三段,为什么不三段而不是五段等等? 因为设一段区间异或和为$w$,那么就存在$w w w w w$,很明显三个相邻的$w$异或之后还是$w$,所以我们一定可以把这个区间分成三段,那么此时的区间是这样的:

设$s[i]$ 为从$1~i$的前缀异或和
很明显: $s[x]=s[r]$, $s[y]=s[l-1]$,且$l<=x<y<r$
所以将前缀异或和存入一个$map$中,然后二分查找$x,y$,如果存在的话,就是答案
// LUOGU_RID: 158064816 #include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n,m; cin>>n>>m; vector<int>a(n+1),s(n+1); map<int,vector<int>>mp; mp[0].push_back(0); for(int i=1;i<=n;i++){ cin>>a[i]; s[i]=s[i-1]^a[i]; mp[s[i]].push_back(i); } while(m--){ int l,r; cin>>l>>r; if(s[r]==s[l-1]) cout<<"Yes"<<'\n'; else{ auto &v1=mp[s[r]],&v2=mp[s[l-1]]; auto it1=lower_bound(v1.begin(),v1.end(),l); auto it2=lower_bound(v2.begin(),v2.end(),r); if(it1!=v1.end()&&it2!=v2.begin()&&*it1<*prev(it2)) cout<<"Yes"<<'\n'; else cout<<"No"<<'\n'; } } cout<<'\n'; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
一道区间DP好题
一开始以为有多种不同的括号匹配次序而导致自己一头大雾wuw,首先看到括号匹配就要想到用栈来求出每个括号对应的匹配项,对于一个区间来说,其左括号一定是具有与之对应的右括号存在时染色才有意义,所以我们要求出每个括号对应的位置$should[i]$,首先设出状态表达式为:$dp[l][r][i][j]$ 也就是 $l~r$ 区间,两端点的染色方案为$i和j$
接下来考虑如何求出方案数:
对于一个相邻的完整括号即:()
这种我们的染色的方案数一定是对于每种情况都是一种,即:
$dp[l][r][0][1]=dp[l][r][1][0]=dp[l][r][2][0]=dp[l][r][0][2]=1$
接下来考虑左括号与右括号相匹配,那么这种情况我们要求在这个区间内的所有括号的染色方案数均被求出,这样我们才能向外扩展,也就是从$l+1$扩展到$l$,对于这种情况下的状态转移如何求?很明显我们只需要考虑某一个端点中的相邻括号的颜色即可,这里拿左端点为例:若左端点$l$为颜色$1$,那么其$l+1$的颜色不可为$1$,那么此时我们有:
$dp[l][r][1][0]=dp[l+1][r-1][2][0]+dp[l+1][r-1][0][1]+dp[l+1][r-1][0][2]$
其它情况同理
对于左端点和右端点的括号不匹配,那么我们就需要寻找左端点对应匹配的位置,并且其相邻位置不应该同色,那么就有:
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1000,mod=1e9+7; int dp[N][N][5][5]; signed main() { std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0); string s; cin>>s; stack<int>stk; int n=s.size(); vector<int>should(n+1); for(int i=0;i<n;i++){ if(s[i]=='(') stk.push(i+1); else should[i+1]=stk.top(),should[stk.top()]=i+1,stk.pop(); } function<void(int,int)>dfs; dfs=[&](int l,int r)->void{ if(l==r-1) dp[l][r][0][1]=dp[l][r][1][0]=dp[l][r][2][0]=dp[l][r][0][2]=1; else if(r==should[l]){ dfs(l+1,r-1); for(int i=0;i<=2;i++){ for(int j=0;j<=2;j++){ if(i!=1) dp[l][r][1][0]=(dp[l][r][1][0]+dp[l+1][r-1][i][j])%mod; if(i!=2) dp[l][r][2][0]=(dp[l][r][2][0]+dp[l+1][r-1][i][j])%mod; if(j!=1) dp[l][r][0][1]=(dp[l][r][0][1]+dp[l+1][r-1][i][j])%mod; if(j!=2) dp[l][r][0][2]=(dp[l][r][0][2]+dp[l+1][r-1][i][j])%mod; } } } else{ dfs(l,should[l]),dfs(should[l]+1,r); for(int i=0;i<=2;i++){ for(int j=0;j<=2;j++){ for(int e=0;e<=2;e++){ for(int k=0;k<=2;k++){ if(j&&e&&j==e) continue; dp[l][r][i][k]=(dp[l][r][i][k]+dp[l][should[l]][i][j]*dp[should[l]+1][r][e][k]%mod)%mod; } } } } } }; dfs(1,n); int res=0; for(int i=0;i<=2;i++) for(int j=0;j<=2;j++) res=(res+dp[1][n][i][j])%mod; cout<<res<<'\n'; return 0; } dp[l][r][1][0]=dp[l+1][r-1][2][0]+dp[l+1][r-1][0][1]+dp[l+1][r-1][0][2];
T42: Problem - D2 - Codeforces
这题真是折磨我,对于$Easy version$ 我们可以暴力的去枚举每个区间,现在考虑如何进行构造
对于三个连续的1来说,那么我们只需要一个 $010$ 即可解决,所以说对于一个没有被满足的1来说,我们直接在他后面放一个1即可,这样暴力求出答案
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; bool cmp(int a,int b){ return a>b; } void solve(){ int n; cin>>n; vector<int>a(n+1); for(int i=1;i<=n;i++) cin>>a[i],a[i]+=i; sort(a.begin()+1,a.end(),cmp); for(int i=2;i<=n;i++) a[i]=min(a[i],a[i-1]-1); for(int i=1;i<=n;i++) cout<<a[i]<<' '; cout<<'\n'; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
对于 $Hard version$,我们肯定不能继续暴力了,那么就要考虑每个点的贡献了,也是求出每个点所拥有的贡献数,由于前面的推导我们知道,对于一个不被满足的i位置,肯定是要在其后面放一个1,这样其后面两个位置均被满足了,也就是说如果我们当前的位置是1,那么有:$dp[i]=dp[i+3]+(n-i)$ 也就是说这个位置的贡献为其上个位置的贡献加上在这个位置放1的贡献,这个位置放1的贡献为 $n-i$,因为i之后的都会用到这个位置的1.
对于当前位置为0:那么有$dp[i]=dp[i+1]$,最后统计的时候把每个位置的贡献都统计上就可以了
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n; string s; cin>>n>>s; int res=0; vector<int>dp(n+4); for(int i=n-1;i>=0;i--){ if(s[i]=='0') dp[i]=dp[i+1]; else{ int j=min(n,i+3); dp[i]=dp[j]+(n-i); } res+=dp[i]; } cout<<res<<'\n'; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
由于操作只能进行一次,并且我们最终的理想串一共就两种,所以我们可以考虑暴力枚举操作位置,然后判断是否能变成理想串,设一个 $pre_i$ 表示 $i$ 位置之前有多少个1,那么我们可以令 $ok_i$ 为在 $i$ 位置之后都是理想串,对于一个长度为 $n$ 的串,我们定义 $ok_{n+1}$ 为满足条件,然后从后向前递推,找到当前位置靠后的第 $k$ 个位置,也就是 $j = min(n+1,i+k)$,那么对于理想串的定义,我们有以下转移方程:
1.$j$ 位置是好的,因为这样我们拼接上才能是好的
2. 从 $i$ 到 $j$ 的这一段长度为 $k$ 的区间内,必须满足全是1或者全是0,即: $pre_j - pre_i = j - i or 0$
3.由于我们跳了 $k$ 个位置,所以我们当前字符必不能与 $j$ 位置的字符相同,即 $s[i]!=s[j]$
至此,我们得到了 $ok$ 数组
接下来直接暴力枚举转移位置 $p$ :
从1位置开始枚举,一遍一遍的去模拟操作肯定是不可以的,由于我们是一步一步来的,所以其操作一定是有序的,换句话来说,对于一次 $p$ 的操作,我们第一个字符一定会到最后一个位置,第 $p$ 个字符一定会成为末尾的第一个字符,对于 $p+1$ 来说,同理,第一个字符的位置还是最后一个,但是新的字符会成为第一个.那么显然的,我们可以在枚举的时候只判断,第1个与第p个是否符合理想串的操作,即:$s_p = \left(\frac{(p-1)}{k} \bmod 2\right) \oplus s_0$,一旦有不符合的,直接退出即可,对于符合的,如果从 $p+1$ 位置是好的也就是 $ok_{p+1}$ 并且同样的转移之后符合上面的公式,那么就是可以进行的:
#include <bits/stdc++.h> #define int long long using namespace std; const int N=1e6+10,mod=1e9+7; void solve(){ int n,k; cin>>n>>k; string s; cin>>s; s=" "+s; vector<int>pre(n+2); for(int i=1;i<=n;i++){ pre[i+1]=pre[i]+s[i]-'0'; } vector<bool>ok(n+2); ok[n+1]=true; for(int i=n;i>=1;i--){ int j=min(n+1,i+k); ok[i]=ok[j]&&(pre[j]-pre[i]==j-i||pre[j]-pre[i]==0)&&(j==n+1||s[i]!=s[j]); } for(int p=1;p<=n;p++){ if(s[p]!=(((p-1)/k%2)^s[1])){ break; } if(ok[p+1]&&(p==n||(s[p+1]==(((n-1)/k%2)^s[1])))){ cout<<p<<'\n'; return; } } cout<<-1<<'\n'; } signed main(){ std::ios::sync_with_stdio(false),cin.tie(0),cout.tie(0);int t;cin>>t;while(t--)solve(); }
令 $c_i$ 为在 $i$ 位置的颜色
首先了解题意, 由于我们只能跨 $1$ ~ $2$步, 故此时如果有 $c_i = c_{i+1}$, 则 $c_i$ 这个颜色是必选的, 若两者不相等, 也必须从两者里面选择出一个来, 那么我们可以给相邻的两点连上一条边, 意味着两个点必须有一个点被选择, 这样就转化成: 第 $1$ 个点和第 $n$ 个点必选, 其余相邻的点必选一个, 那么就可以转化成最小点覆盖问题, 我们可以把 $m$ 个点对半分, 然后枚举出前 $\frac{m}{2}$ 个点的状态, 然后再和剩下的点进行合并.
* 在枚举过程中, 会出现状态某两个点没有选择, 而我们必须要在这两个点中选择一个点, 所以我们此时的情况是无解的, 直接设置成无穷大即可
* 需要倒着枚举状态, 因为即使出现无解的情况, 我们也要对其进行更新, 也就是符合情况的最小的超集, 例如存在两个点没有选择, 而这两点必须选择一点, 那么我们首先要把这个状态设置成无穷大, 然后去枚举所有没有选择的点, 对他的超集, 也就是符合条件的进行更新, 这样做是为了在合并的时候, 只要后半段有解, 前半段即使无解, 由于我们枚举了超集, 再加上某个点使得这个状态最小, 从而合并
* 时间复杂度$O(2^{\frac{m}{2}} * (\frac{m}{2})^2)$
#include <bits/stdc++.h> #define int long long using namespace std; const int INF = 1e18, mod = 1e9 + 7; signed main() { std::ios::sync_with_stdio(false), cin.tie(0), cout.tie(0); int n, m; cin >> n >> m; vector<int> c(n + 1), cost(m + 1); for(int i = 1; i <= n; i++) cin >> c[i]; for(int i = 1; i <= m; i++) cin >> cost[i]; vector<vector<bool>> a(m + 1, vector<bool> (m + 1)); for(int i = 1; i <= n; i++){ if(i > 1) a[c[i]][c[i - 1]] = true; if(i < n) a[c[i]][c[i + 1]] = true; } a[c[1]][c[1]] = a[c[n]][c[n]] = true; int z1 = m / 2, z2 = m - z1; vector<int> dp(1LL << (z1 + 1)); for(int i = (1LL << z1) - 1; i >= 0; i--){ // O(2 ^ (m / 2) * (m / 2) * (m / 2)) bool ok = true; for(int j = 1; j <= z1; j++){ if((i >> (j - 1) & 1) == 0){ for(int k = 1; k <= z1; k++){ if((i >> (k - 1) & 1) == 0 && a[j][k]){ ok = false; } } } } if(ok){ for(int j = 1; j <= z1; j++){ if((i >> (j - 1) & 1)){ dp[i] += cost[j]; } } } else{ dp[i] = INF; for(int j = 1; j <= z1; j++){ if((i >> (j - 1) & 1) == 0){ dp[i] = min(dp[i], dp[i ^ (1 << (j - 1))]); } } } } int res = INF; for(int i = 0; i <= (1LL << z2) - 1; i++){ bool ok = true; for(int j = 1; j <= z2; j++){ if((i >> (j - 1) & 1) == 0){ for(int k = 1; k <= z2; k++){ if((i >> (k - 1) & 1) == 0 && a[z1 + j][z1 + k]){ ok = false; } } } } if(!ok) continue; int cur = 0, should = 0; for(int j = 1; j <= z2; j++){ if((i >> (j - 1) & 1)){ cur += cost[z1 + j]; } else{ for(int k = 1; k <= z1; k++){ if(a[k][j + z1]){ should |= (1LL << (k - 1)); } } } } res = min(res, cur + dp[should]); } cout << res << '\n'; return 0; }

浙公网安备 33010602011771号