Practice on Codeforces and Atcoder in June
wk,误删了4个题,但我不想补了
题意:给一个正整数序列
首先,我们可以精确地将球放进最终位置,那么就上限操作次数就是
然后考虑计算。我们称将球移动到最终位置为删点操作
显然一个0球可以使得一个区间变得有序,而这个区间实际上是往左往右扩展到的第一对递增的的球
进而我们就可以得到,一个
则其代价就是
这么简单?是不是求个上升子序列就行了?
不不不
两个挨着的固定点,其中一个是可以不管的,所以事实上是一个连续的递增序列算一个就可以了
那考虑设
显然有:
初始:
转移:
答案:
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
using namespace std;
#define N 505
int f[N][N],n,m,t,a[N];
int main(){
ios::sync_with_stdio(false);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
int tag=1;f[0][0]=0;
for(int i=1;i<=n;i++){
tag&=(a[i]>a[i-1]);
f[0][i]=0;
f[i][0]=tag?0:0x3f3f3f3f;
}
for(int j=1;j<=n;j++){
for(int i=1;i<=n;i++){
f[i][j]=f[i][j-1];
if(a[i-1]<a[i])f[i][j]=min(f[i][j],f[i-1][j]);
for(int k=0;k<i-1;k++){
if(a[k]<a[i]){
f[i][j]=min(f[i][j],f[k][j-1]+i-1-k);
}
}
}
}
for(int i=1;i<=n;i++){
int ans=f[n][i];
for(int j=1;j<=n;j++){
ans=min(ans,f[j][i-1]+n-j);
}
cout<<ans<<" ";
}cout<<"\n";
}
}
考场上读错题是直接心态爆炸的
首先注意到,无论怎么往返走,括号序列长度的奇偶性始终是不变的,那么如果
现在来考虑什么情况下是绝对无解的
注意到如果一个相邻括号不同的序列,无论怎么往返走,其始终是合法的(因为增量是一个合法的串),那么我们只需要考虑连续的左/右括号段。显然如果最前面的连续括号段是右括号段,则后面无论怎么搞也补不回来这些左括号。同理,如果后面是连续的左括号,也不论怎么走也是搞不回来这些右括号。所以得到了必要条件:第一个连续括号段是左括号段,最后一个连续括号段是右括号段
尝试证明其是充要条件:在这些段里,可以提前补足所有后面/前面缺的括号,那么这就是充分的
详细的说,我们使用头尾两个连续括号段,可以使得序列里所有的连续括号段长度变为
至于括号位置的维护,可以使用 set
int main(){
set<int>a;
int n,m;cin>>n>>m;
for(int i=1;i<=n;i++){
char x;cin>>x;
if((i%2)^(x=='('))a.insert(i);
}
while(m--){
int x;cin>>x;
if(a.count(x))a.erase(x);
else a.insert(x);
if(n%2)cout<<"No\n";
else if(a.size()&&(*a.begin()%2||*a.rbegin()%2==0))cout<<"No\n";
else cout<<"Yes\n";
}
return 0;
}
给定一个长为
求满足条件的
计数题,考虑进行DP。
题目换个描述就是,找有多少个
设
显然有
注意到
复杂度
写出这个递推公式之后,可以看到它与原来的
假设全部把它变成
这是简单的,长为
那么答案即为:
发现时间复杂度仍然是
正难则反,注意到所有的序列个数是
那么选不出
所以可以得到:
还能推出一个组合恒等式:
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int p=1e9+7;
int power(int a,int b){
int ans=1;
while(b){
if(b&1)ans=a*ans%p;
a=a*a%p;
b>>=1;
}
return ans;
}
int a[1005050],n,k,m,t;
signed main(){
ios::sync_with_stdio(false);
cin>>t;
while(t--){
cin>>n>>m>>k;
for(int i=1;i<=n;i++)cin>>a[i];
int mul=1;
int ans=0;
for(int i=0;i<n;i++){
mul=mul*(m-i+1)%p*power(i,p-2)%p;
if(i==0)mul=1;
ans=(ans+power(k-1,m-i)*mul%p)%p;
}
ans=power(k,m)-ans;
ans=(ans%p+p)%p;
cout<<ans<<"\n";
}
}
考场上这破题居然没做出来。
首先,考场思路是:
先处理出值和正负号,设
对于一个数进行更改,有且只有三种情况。
- 把自身变大,这样可能会导致前面的正数中某部分变成负数。
- 把自己变小,这样可能会导致前面的负数中某部分变成正数。
- 对前面的数没有影响。
进一步思考,我们得到:设
那么就有:
,此时不会对前面的产生影响。 ,此时会导致 变成负数。 ,注意,如果 中不存在 使得 ,则会导致 里所有的正数变成负数。其中 定义为是满足 的最大整数。 ,这种情况只是让前面部分变成负数。
念至此,此题似乎用前缀和,套几个指针维护即可。
但,代码难度是较高的。赛场上的我就直接开莽,然后掉大分。
面对这种想到可做思路但代码难度较高时,可以再用五到十分钟思考是否还有更加优秀的性质。
显然,这个题,是有的。
回退到三种最初的影响情况,运用 比较两个等价决策 的方法(这是贪心的常用方法)。
对于第一种情况,不难发现实际上这个变成负数的量,只取决于改后的权值,初始权值和当前的位置。
而在初始权值相同的情况下,一定是位置最靠前的最优,直接处理出每个权值第一次出现位置,暴力枚举改后权值跑即可,这样的位置至多只有五个。
不难看出,对于第二种情况,把自己变小对前面造成影响,能造成影响当且仅当自身是后缀最大值,且只含这样一个最大值。
那么这个点,等价于是后缀最大值在更新的时候遇到的点。权值只有五种,那么这样的点也至多只有五个,暴力枚举改后的权值暴力跑即可。
第三种情况是容易统计的,在统计后缀最大值的时候处理即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
void read(int &x){
int w=1;x=0;char ch=getchar();
while(ch>'9'||ch<'0'){
if(ch=='-')w*=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9'){
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
x*=w;
}
int n,m,t;
char a[505050];
int val[505050],pos[505050],w[5]={1,10,100,1000,10000},cnt[5];
int solve(){
int mx=val[n],ans=0;
for(int i=n;i;i--){
if(val[i]>=mx)ans+=val[i],mx=val[i];
else ans-=val[i];
}
return ans;
}
signed main(){
/*
不能操作的赢
那么肯定是尽量少
*/
ios::sync_with_stdio(false);
int t;cin>>t;
while(t--){
cin>>a+1;n=strlen(a+1);cnt[0]=cnt[1]=cnt[2]=cnt[3]=cnt[4]=0;
for(int i=1;i<=n;i++){
pos[i]=a[i]-'A';val[i]=w[pos[i]];
}
int mx=a[n];int s=solve(),d=0,p=s;
for(int i=1;i<=n;i++){
cnt[pos[i]]++;
if(cnt[pos[i]]==1){
int v=val[i];
for(int j=0;j<5;j++){
val[i]=w[j];s=max(s,solve());
}
val[i]=v;
}
}
for(int i=n;i;i--){
if(val[i]>mx){
int v=val[i];
for(int j=0;j<5;j++){
val[i]=w[j];s=max(s,solve());
}
val[i]=v;mx=val[i];
}
else if(val[i]<mx){
d=max(d,mx+val[i]);
}
}
cout<<max(s,d+p)<<"\n";
}
}
我觉得,这题没有C难。
说说思路
我最初的想法是看到
对于这样的选择问题,排序是很容易想到的。
注意到一对线段中靠后的线段对后面的选择起了决定性作用,而当我们将所有线段按右端点排序之后会发现对后面的线段的影响,越往后影响越大,这是显然的。
那么容易想到贪心策略:如果可能的话,这对线段中靠后线段尽可能靠前,因为这样会让后面的线段自由性更大。
接着就可以想到,如果枚举当前线段,则最优匹配线段就是与其相交的后面的最近的一个线段,这个可以通过指针处理
这时候我们反过来考虑,对于一个确定的靠后线段,要凑成对,肯定是尽量靠近它才对撒。
解决方案已经出来了:
- 将所有线段按右端点排序
- 记录
分别表示上一对线段的靠后线段的右端点,当前枚举到的符合条件的左端点的最远右端点 - 依次枚举每一条线段
- 如果
,说明这个线段已经被删了,跳过 - 如果
,说明现在构成了一对合法线段,将答案加上, - 如果4,5都不满足,令
即可
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i].l>>a[i].r;
sort(a+1,a+n+1);
int lst=-1,mx=-1,ans=0;
for(int i=1;i<=n;i++){
int l=a[i].l,r=a[i].r;
if(l<=lst)continue;
if(mx>=l){
++ans;lst=a[i].r;mx=-1;
}
else {
mx=r;
}
}
cout<<n-ans-ans<<"\n";
对于选择一个长为
自然,每一次肯定是贪心选取最长的段进行填充,那么怎么高效去处理这个过程呢?
考虑令
注意到如果矩形的高度单调递增,则我们可以很方便地统计出每一个可能的宽度有多少高度(前后高度差),并将其累计
那么如何将矩形化为这种情况呢?
单调栈
我们考虑维护一个高度递增的矩形集合,每次找第一个(也即高度最大的一个),如果新矩形比它高,直接插入,否则以栈顶为起点,把高度比这个新矩形高的答案先统计掉,然后加上这个新矩形,宽度为
大致的思路是这样的,代码还有一些细节。
#include<iostream>
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 505050
#define int long long
int top,n,a[N],t,m,cnt[N];
struct node{
int h,w;
}sta[N];
signed main(){
ios::sync_with_stdio(false);
cin>>t;
while(t--){
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
cin>>m;
sta[0].h=0;
for(int i=1;i<=n;i++)a[i]=n-a[i];
top=0;for(int i=1;i<=n;i++)cnt[i]=0;
for(int i=1;i<=n;i++){
if(top==0||sta[top].h<a[i])sta[++top]=(node){a[i],1};
else if(a[i]==sta[top].h)sta[top].w++;
else{
int w=0;
while(top&&sta[top].h>a[i]){
w+=sta[top].w;
cnt[w]+=sta[top].h-max(sta[top-1].h,a[i]);--top;
}
if(top&&sta[top].h==a[i])sta[top].w+=w+1;
else sta[++top]=(node){a[i],w+1};
}
}
int w=0;
while(top){
w+=sta[top].w;
cnt[w]+=sta[top].h-sta[top-1].h;--top;
}
int ans=m;
for(int i=n;i;--i){
// cout<<i<<' '<<cnt[i]<<" matix\n";
if(m>cnt[i]*i)m-=cnt[i]*i,ans-=cnt[i];
else{
ans-=(m+i-1)/i;
break;
}
}
cout<<ans<<"\n";
}
}
CF1824B2
首先,设
则
考虑如何求出
我们回到定义,考虑一个点是好点的充分必要条件是什么。
树是无根树,这就能带来一个条件:将一棵树中任意一个点当做根来考虑不会影响最终结果,且一般会使得思考过程更加简单
求解一个最优化问题的充要条件,我们常常会使用比较两个决策的方法
假设在当前方案中,存在一个好点
我们考虑将点
如果
答案不会变劣,当且仅当
显然当
考虑当
我们首先来讨论不存在
这显然等价于
我们更改
这个情况,怎么看呢?
显然就是强制性令其中一个
这个方案数是什么?显然可以列出式子:
对的吗?不对,会重复统计
这一步,我们以退为进,去掉我们的假想条件
我们考虑
其实我们可以在
为什么,根据
因为如果在这条链中间,插入一个虚点作为根,则必定会存在两个节点,他们的
这两个节点的路径上的点,全部都是好点
所以
到最后就有:
CF1824C
这个题,难度主要在算法上
不难想到设
其中
显然,我们应该让众数不变,其他变成众数,这样的
然后最后只保留一个众数即可
注意到如果有多个众数,将数保留,但是代价仍然只计算
那么我们如何实现求众数的操作呢?
考虑暴力解决这个问题
设集合
这样的数据结构,不难想到map<int,int>
,用迭代器遍历暴力合并即可。
这样肯定会TLE……,因为复杂度可以卡到
发现这个问题可以使用启发式合并优化,我们每次在合并集合
#include<iostream>
#include<cstdio>
#include<map>
using namespace std;
#define N 505050
map<int,int>cnt[N],tmp;
int ans,n,id[N],a[N],head[N],ver[N<<1],nxt[N<<1],tot;
void dfs(int u,int fa){
int res=0,mx=1;
for(int i=head[u];i;i=nxt[i]){
int v=ver[i];
if(v==fa)continue;
a[v]^=a[u];++res;
dfs(v,u);
if(cnt[id[v]].size()>cnt[id[u]].size())swap(id[v],id[u]);//important
for(auto x:cnt[id[v]]){
cnt[id[u]][x.first]+=x.second;
mx=max(mx,cnt[id[u]][x.first]);
}
}
if(!res){
cnt[id[u]][a[u]]++;return ;
}
ans+=res-mx;
tmp.clear();
if(mx>1){
for(auto x:cnt[id[u]]){
if(x.second==mx){
tmp[x.first]=1;
}
}
swap(tmp,cnt[id[u]]);
}
}
void add(int u,int v){
nxt[++tot]=head[u],ver[head[u]=tot]=v;
}
int main(){
ios::sync_with_stdio(false);
cin>>n;
for(int i=1;i<=n;i++)id[i]=i;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<n;i++){
int u,v;cin>>u>>v;
add(u,v);add(v,u);
}
dfs(1,0);
cout<<ans+(cnt[id[1]][0]==0);
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!