A-Array Balancing
题意:给定两个数组a,b,a数组第i个数可以和b数组的第i个数交换,最终求
a1−a2|+|a2−a3|+⋯+|an−1−an||a1−a2|+|a2−a3|+⋯+|an−1−an| ++ |b1−b2|+|b2−b3|+⋯+|bn−1−bn||b1−b2|+|b2−b3|+⋯+|bn−1−bn|的最小值;
总结:第一道就dp一开始不太敢写
思路:对于数组a1 a2 a2....... an
b1 b2 b3........bn
当我们遍历到第i个数时,第i-1有两种情况分别为交换和不交换,i也有两种情况为交换和不交换。
于是我们可以将问题分解为多个子问题,对于第i对数,我们选择不交换的话,就是取i-1中交换与不交换两者中的最小值,同理选择交换的话也是,于是我们可以写出状态转移方程(代码容易看懂)
tip:注意开long long ,别问问就是wa了一发
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
int t;
cin>>t;
while(t--){
ll n,a[30],b[30],dp[30][2]={0};
cin>>n;
for(int i=1;i<=n;i++)cin>>a[i];
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=2;i<=n;i++){
dp[i][0]=min(dp[i-1][0]+abs(a[i]-a[i-1])+abs(b[i]-b[i-1]),dp[i-1][1]+abs(a[i]-b[i-1])+abs(b[i]-a[i-1]));
dp[i][1]=min(dp[i-1][0]+abs(a[i]-b[i-1])+abs(b[i]-a[i-1]),dp[i-1][1]+abs(a[i]-a[i-1])+abs(b[i]-b[i-1]));
}
ll ans=min(dp[n][0],dp[n][1]);
cout<<ans<<endl;
}
}
B-Getting Zero
题意:给定一个整数v,可以进行两个操作,操作1:v=(v+1)%32768,操作二:v=(2*v)%32768;
给定n个整数ai,0<=ai<=32768;问最少要多少次操作可以把这个数变为0;
思路:对于这道题我们必须从32768这个特殊的数字考虑,为什么偏偏是他呢,他肯定有特别的地方,于是我们尝试分解他之后发现他恰好是2^15,知道这个性质这个题就好做了,对于任意一个数,他最多也就进行15次操作二后得到0,但这并一定是最优解呀,我们发现如果我们将一个数分解成(2^cnt)*k,那么这个数就只需要进行15-cnt次了,所有对于给定的ai,我们只需要遍历ai~ai+15,再对这个数字求2的因子的个数后求总的操作次数,就可以得到每个数的答案了,然后在这些答案中取最小值即可。
tip:注意0的时候操作次数就是0的而不是15,所以要特判掉。(别问问就是wa了一发)
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
int main(){
int n;
cin>>n;
while(n--){
int a;
cin>>a;
int ans=15;
for(int i=a;i<=a+15;i++){
if(i>0){
int tmp=i;
int cnt=0;
while(tmp%2==0)cnt++,tmp/=2;
int res=i-a+(15-cnt);
ans=min(ans,res);
}
}
if(a==0)ans=0;
cout<<ans<<" ";
}
}
C-Water the Trees
题意:给定n个数,从第一天开始若该天为奇数则可以对一个数加1,若该天为偶数则可以说对一个数加2,问最少需要多少天才可以将所有数都变为相同的数,注意对该天我们可以选择不操作,即不对任何数加数。
总结:爆哭,12.37分过的35分交的代码打错了一个数字呜呜呜
思路:这道题是贪心的思路,表面上看挺复杂的,但我们将问题简化一下就比较容易看出问题,简化完将会变成我们常见的一个贪心问题,首先我们需要知道所有数只能加不能减啊,所有最终的答案所有序列至少也得是原序列中的最大值,但是一定是原序列中的最大值吗,这就不一定啦,比如有一个序列7778777,对于这个序列我们发现最终答案序列为9的比8的序列需要的天数还要小,如果为10呢?10的话相当于在原序列上增加3332333,对比8的答案我们发现8是1110111,10的答案要比8的答案每个数都多增加了2,肯定会比8的答案多,于是我们可以知道最终答案是在最大值和最大值加1的序列中的最小值。
这里我们设最大值为ma;
于是我们可以开始简化问题,对于一个序列,我们比较关心的是他可以进行加二的操作的次数,因为对于加1的操作不能由加2实现,但加2的操作却可以由加1实现。所有我们就可以对整个序列进行遍历啦,如果该数为奇数,那么可以加1的操作数就加1,最终得到cnt1,我们还要处理出最终达到答案序列总共需要的数字和totle,加2的操作数就为(totle-cnt1)/2;
到这里我们就可以很明显得看出来是一个贪心问题了,我们需要判断的是进行两天(即加3)的一个循环的次数,即totle/3,但所有的循环次数里又不一定每一次都能实现加2的操作,此时cnt2的作用就出来了。我们贪心最大的二的操作次数,剩余的数我们就只能进行循环每两天加1了。
总而言之就是分类讨论,还是比较容易讨论的,主要是简化问题比较复杂,代码中的分类讨论已经很清晰了。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
const int N=3e5+10;
ll h[N];
int main(){
ll t;
cin>>t;
while(t--){
ll n,ma=-1;
cin>>n;
for(int i=1;i<=n;i++)cin>>h[i],ma=max(ma,h[i]);
//最大值为ma的操作
ll ans=0,cnt1=0,cnt2=0,totle=0;
for(int i=1;i<=n;i++){
ll cha=ma-h[i];
if(cha%2==1)cnt1++;//可以进行+1的操作
totle+=cha;//处理出总共需要的数字和
}
cnt2=(totle-cnt1)/2;//可以进行+2的操作
if(totle/3<=cnt2){//此时每两天一个循环中所有操作都能进行(即加3)
ans=totle/3*2;//循环次数*2(即为天数)
if(totle%3==1)ans++;//剩余1个,那直接加1天即可
else if(totle%3==2&&cnt2>totle/3)ans+=2;//剩余两个,就得看cnt2了
else if(totle%3==2&&cnt2==totle/3)ans+=3;
}
else {
ans=totle/3*2;
ll less=totle-cnt2*2-totle/3;//剩余的数字和
ans+=2*less-1;//只能每两天加1,最后减掉最后一个循环的偶数天
}
//最大值为ma+1的操作
ll ans1=0;
cnt1=0;cnt2=0;totle=0;
for(int i=1;i<=n;i++){
ll cha=ma-h[i]+1;
if(cha%2==1)cnt1++;
totle+=cha;
}
cnt2=(totle-cnt1)/2;
if(totle/3<=cnt2){
ans1=totle/3*2;
if(totle%3==1)ans1++;
else if(totle%3==2&&cnt2>totle/3)ans1+=2;
else if(totle%3==2&&cnt2==totle/3)ans1+=3;
}
else {
ans1=totle/3*2;
ll less=totle-cnt2*2-totle/3;
ans1+=2*less-1;
}
cout<<min(ans1,ans)<<endl;
}
}
星期一补D题今晚先学专业课吧呜呜呜
来啦补题
D. Progressions Covering
题意:给定一个全为0的a序列和一个b序列,每次可以对a序列选择一串小于等于k的字串按顺序加上1,2,3,4,5......k,问最少进行多少次操作能使得a数组的每一个都大于等于b数组的每一个。
知乎上看的题解有有线段树的和没线段树的,想到线段树可能比较好理解就是代码难搞点,还是搞线段树吧,好久没用过线段树都快忘了,顺便熟悉一下线段树板子吧。
思路:如果我们从前往后枚举,则第一个数加1,第二数加2一直累加,我们发现对于遍历到的这个数并不是最优解,但是从后往前的话,第i个数加k,第i-1个数加k-1一直累积,这样子的贡献在保证能把这个数加到最大的同时还能对前面的数实现最大的贡献,于是这是最优解。我们又发现这是一个1,2,3的序列,恰好是一个差分序列为1的数组,我们只需要用线段树访问该差分数组前缀和就可以找到他当前的值,再进行模拟即可。
代码:
#include<bits/stdc++.h>
using namespace std;
#define ll long long
#define int long long
const ll N=3e5+10;
ll b[N];
struct node{
ll l,r;
ll sum,add;
}tr[N<<2];
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void bt(int u,int l,int r){
if(l==r)tr[u]={l,l,0,0};
else {
tr[u]={l,r,0,0};
int mid=l+r>>1;
bt(u<<1,l,mid);
bt(u<<1|1,mid+1,r);
pushup(u);
}
}
void pushdown(int u){
tr[u<<1].sum+=tr[u].add*(tr[u<<1].r-tr[u<<1].l+1);
tr[u<<1|1].sum+=tr[u].add*(tr[u<<1|1].r-tr[u<<1|1].l+1);
tr[u<<1].add+=tr[u].add;
tr[u<<1|1].add+=tr[u].add;
tr[u].add=0;
}
void update(int u,int l,int r,int d){
if(l<=tr[u].l&&tr[u].r<=r){
tr[u].sum+=d*(tr[u].r-tr[u].l+1);
tr[u].add+=d;
}else {
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
if(l<=mid)update(u<<1,l,r,d);
if(r>mid)update(u<<1|1,l,r,d);
pushup(u);
}
}
ll query(int u,int l,int r){
if(l<=tr[u].l&&tr[u].r<=r){
return tr[u].sum;
}
else {
pushdown(u);
int mid=tr[u].l+tr[u].r>>1;
ll res=0;
if(l<=mid)res+=query(u<<1,l,r);
if(r>mid)res+=query(u<<1|1,l,r);
return res;
}
}
signed main(){
int n,k;
ll ans=0;
cin>>n>>k;
bt(1,1,n);
for(int i=1;i<=n;i++)cin>>b[i];
for(int i=n;i>=1;i--){
ll now=query(1,1,i);
if(now>=b[i])continue;
int last=min(k,i);//注意这里要判断他的区间是否会小于1。
ll time=(b[i]-now+last-1)/last;
time=max(time,0ll);
ans+=time;
update(1,i-last+1,i,time);
}
cout<<ans;
}
加油加油,e题这两天看看能不能补吧,上一场div2还没写题解呜呜呜。最晚周四补掉e题,好像挺难的,还是周五吧呜呜呜。也是线段树呜呜呜。
来啦明天没课但是我今晚补掉了,并查集加线段树的做法我是真的不会呜呜呜,好难。
于是上知乎学了一个,不过也学到了一个询问区间线段个数的前缀和技巧,不亏不亏。
来补啦,可以看这个Educational Codeforces Round 126 (Rated for Div. 2) A~E - 知乎
我写写我的总结呜呜呜。
E-
题意:给定一个3*n的01矩阵,进行q个询问,问3*(l,r)的这个区间内有多少个1的连通块
思路:上面发的知乎的题解非常棒。在此我为了更容易理解加入一些解释。
首先,我们可以比较清晰的一点是,对于我们要查询的区间,会影响答案的只能是在两端,中间的连通块是可以直接由前缀o(1)求出的,
其次,对于两端,我们要考虑有什么情况会影响到答案,我们暂且只考虑左边,对于左边
l-1为0 0 0,肯定不会影响答案,
l-1为1 0 0,呢?我们稍微思考可以发现 无论l是什么答案都不会被这个影响;
l-1为0 1 0,呢?同样不会
0 0 1 不会
1 1 0 不会
1 0 1 不会
0 1 1 不会
1 1 1 会
我们发现只有111才可能使得答案被影响,那他是如何影响的呢
l r
1 1 1 1 1
1 0 0 0 0
1 1 1 1 1 我们发现只有这样的情况才可能在截取区间时将原本的一块的答案变成两块
对于右边也是一样。
那介于两者之间呢?
l r
1 1 1 1 1
1 0 0 0 1
1 1 1 1 1
我们发现这种情况就更加特殊了,只有介于两者中间才会使一块的答案变成两块.
所有我们最终要处理的东西就只有这三种特殊的情况,其他的都可以用一般的线段求前缀来得到。
代码:
#include<bits/stdc++.h>
using namespace std;
#define pii pair<int,int>
#define pb push_back
const int N=5e5+10;
int n,dx[]={-1,0,0,1},dy[]={0,-1,1,0},pre1[3][N],pre2[3][N];
vector<pii> v[3];
bool vis[3][N];
string s[3];
int pos[N];
pii bfs(int x,int y,int col){
int l=y,r=y;
vis[x][y]=true;
queue<pii>q;
q.push({x,y});
while(q.size()){
pii tmp=q.front();q.pop();
int xx=tmp.first,yy=tmp.second;
for(int i=0;i<4;i++){
int nx=xx+dx[i],ny=yy+dy[i];
if(nx>=0&&nx<3&&ny>=0&&ny<n&&!vis[nx][ny]&&s[nx][ny]=='1'){
q.push({nx,ny});
vis[nx][ny]=col;
l=min(l,ny);
r=max(r,ny);
}
}
}
pii line={l,r};
return line;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n;
for(int i=0;i<=n;i++)pos[i]=1e9;
for(int i=0;i<3;i++)cin>>s[i];
//先用bfs处理出所有存在的线段。
for(int i=0;i<3;i++){
for(int j=0;j<n;j++){
if(!vis[i][j]&&s[i][j]=='1')v[0].pb(bfs(i,j,i*n+j+1));
}
}
//for(int i=0;i<v[0].size();i++)cout<<v[0][i].first<<" "<<v[0][i].second<<endl;
//接着找到第二种情况,即为101的每一列的模拟
int cnt=0;//记录连续的101的个数
for(int i=0;i<=n;i++){
if(i!=n&&s[0][i]=='1'&&s[1][i]=='0'&&s[2][i]=='1'&&vis[0][i]==vis[2][i])cnt++;
else if(cnt>0){
int l=i-cnt;
int r=i-1;
int is_l= l>0&&s[0][l-1]=='1'&&s[1][l-1]=='1'&&s[2][l-1]=='1';
int is_r= r<n-1&&s[0][r+1]=='1'&&s[1][r+1]=='1'&&s[2][r+1]=='1';
if(is_l && is_r){
for(int i=r;i>=l;i--)pos[i]=l;
}//pos[r]=l代表右端点是r是左端点至少得是l才会有加一的贡献;
//如 1 1 1 1 1
// 1 0 0 0 1
// 1 1 1 1 1
// l r
// L必须大于l才会有加一贡献 (即将整个1串分成两部分)
if(is_l && !is_r)v[1].pb({l,r});
// 1 1 1 1 1 ?
// 1 0 0 0 0 ?
// 1 1 1 1 1 ?
// l r
// 这种情况可以同样可以用线段前缀的方式来得到答案
if(!is_l && is_r)v[2].pb({l,r}); //同上不过是在r右边
cnt=0;
}
}
//处理前缀
for(int i=0;i<3;i++){
for(int j=0;j<v[i].size();j++){
int l=v[i][j].first,r=v[i][j].second;
pre1[i][l]++;//在l处进入的线段
pre2[i][r+1]++;//在r处离开的线段
}
for(int j=1;j<n;j++)pre1[i][j]+=pre1[i][j-1],pre2[i][j]+=pre2[i][j-1];
}
int q;cin>>q;
while(q--){
int x,y;
cin>>x>>y;
x--;y--;
int ans;
ans = pre1[0][y] - pre2[0][x] + pre1[1][x] - pre2[1][x] +pre1[2][y] - pre2[2][y] ;
if(x>=pos[y])ans++;
cout<<ans<<endl;
}
}
f就算了,明天起来补上一场的