2022XCPC训练刷题笔记
虽然是按日期做的题解,但是不一定那天只做了这点题(坚定的眼神)
以前做的题
CF1562A 数学\取模
给范围L到R,求里面两个数取模最大值
首先肯定是b = R时有最大解,令x = b/2。当a小于x时,b里可以装下两个及以上的a,模数也就很小了。要使模数最大,直接模x+1即可
int main() {
cin >> t;
while (t--) {
cin >> l >> r;
cout << r % max(r / 2 + 1, l) << '\n';
}
return 0;
}
CF edu123 C 最大子段和+整体化思想
每次修改是加一个x进去,加到原序列某个位置上,不能重复加。修改n次,每次求一个最大子段和
我把n方毙了,实际上数据是恰好,正解n方。
考虑特殊性质,x不变且为正数,应该贪心地加入它。第i次修改,那么这次的最大子段和的序列里面一定包含了i*x。
求不同长度的最大子段和,遍历i的时候,遍历不同长度j的最大子段和,往里面加入i*x再求最优,最优答案可能在遍历j的外面,因此注意最优答案是在整体状态空间上的。
while(t--)
{
scanf("%d%d",&n,&x);
for(int i=1;i<=n;i++)
{
scanf("%d",a+i);
sum[i] = sum[i-1] + a[i];
f[i] = -0x7f7f7f7f;
}
for(int i=1;i<=n;i++)
for(int j=i;j<=n;j++)
f[j-i+1] = max(f[j-i+1],sum[j]-sum[i-1]);//不同长度最大子段和
int ans = 0;
for(int i=0;i<=n;i++)
{
for(int j=i;j<=n;j++)
ans = max(ans, f[j]+i*x);
printf("%d ",ans);
}
puts("");
}
子串的最大差 单调栈+计算贡献
我现在觉得单调栈就不要纠结单调增还是单调减了
要求得每个元素在多少个子串中是最大值
对于单个元素:(左边比它小的个数,小于等于)* (右边比它小的个数,仅小于)
直接用栈来保存位置,对于ai,一直弹出栈顶元素直到栈顶比它大,这样就找到了最靠左的位置使得ai在这个区间里最大,又从右往左做一遍
最小值同理
int n,a[N];
int posl[N],posr[N];//记录左右两边更大值的位置
int stk[N],top;
ll ans = 0;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",a+i);
a[0] = a[n+1] = inf;
for(int i=1;i<=n;i++)
{
while(a[stk[top]] <= a[i])//单调增找最大
top--;
posl[i] = stk[top];
stk[++top] = i;//存位置
}
top = 0;
stk[0] = n+1;
for(int i=n;i>=1;i--)
{
while(a[stk[top]] < a[i])//避免重复
top--;
posr[i] = stk[top];
stk[++top] = i;
}
for(int i=1;i<=n;i++) ans += 1ll * (posr[i]-i) * (i-posl[i]) * a[i];
//负的贡献再做一遍
a[0] = a[n+1] = -inf;
stk[0] = 0;
for(int i=1;i<=n;i++)
{
while(a[stk[top]] >= a[i])//单调减找最小
top--;
posl[i] = stk[top];
stk[++top] = i;//存位置
}
top = 0;
stk[0] = n+1;
for(int i=n;i>=1;i--)
{
while(a[stk[top]] > a[i])
top--;
posr[i] = stk[top];
stk[++top] = i;
}
for(int i=1;i<=n;i++) ans -= 1ll * (posr[i]-i) * (i-posl[i]) * a[i];
printf("%lld",ans);
}
CF793D 区间dp
从外往里做的一个区间dp,设ij是可以去的区间
https://www.luogu.com.cn/blog/xzggzh/solution-cf793d
细节太多了,好难受
(鸽巢原理)从n个数中选出几个数和为n的倍数
题意:
给定一个长度为$$n(n\le 10^5) $$的数组a,你需要选择其中一些数之和为n的倍数
思路:
首先n的倍数模n为0,考虑到选数,利用求模线性的特点,我们把不同可能性的和限制在0到n之间
这就引出了抽屉原理(有点靠直觉),同时要想到前缀和,由于只关心0到n之间的值,前缀和通通模上n
-
若前缀和模以n为0,那么从第一个数到他之和就是答案
-
前缀和两个相同数位置之间的这一段和就是答案,因为这意味着这一段的和模以n得0
-
前缀和数组算上第0位有n+1个数字,由于抽屉原理一定有两个相同的数,一定有有解
为了找这个相同的,用个桶就行
void solve()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
pre[i] = (pre[i-1] + a[i]) % n;
}
int ansl = 0,ansr = 0;
for(int i=1;i<=n;i++)
{
if(pre[i] == 0)
{
ansl = 1,ansr = i;
break;
}
if(pos[pre[i]])
{
ansl = pos[pre[i]] + 1, ansr = i;//别忘了前缀和对应的区间
break;
}
else pos[pre[i]] = i;
}
printf("%d\n",ansr-ansl+1);
for(int i=ansl;i<=ansr;i++)
{
printf("%d ",i);
}
}
离线序列操作+利用性质
题意:
给序列,操作1:把第$$x$$个数改为\(y\);操作2:把所有小于\(y\)的数覆盖为\(y\)
思路:
误区:一下进入数据结构加维护的思路里,忽略了各种操作的后效性
考虑离线化操作,看看操作的性质,对于覆盖的操作2,只有改成最大的值答案才能留在最后,对于操作1,最后一次对\(x\)的操作才会留在最后。注意到无论操作1还是2,都会对x位置进行一个尝试修改
那么如何处理两种操作让他们的相互影响在离线化的情况下被统计呢?
由于有“最值”、“最后”这种字眼,我们倒着列举操作,保存最大的覆盖值,往前看操作,如果是更小的覆盖就没用,如果对当前值的修改更小也没用
对于从来没有过修改的值输出$$a[i]$$即可
我们用一个$$ans$$数组保存修改后的答案,若为空则看是$$maxcover$$大还是$$a[i]$$大
void solve()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
for(int i=1;i<=q;i++)
{
scanf("%d",&op[i][0]);
if(op[i][0]==1) scanf("%d%d",&op[i][1],&op[i][2]);
else scanf("%d",&op[i][1]);
}
int maxcover = -1;
for(int i=q;i>=1;i--)
{
if(op[i][0] == 2) maxcover = max(maxcover,op[i][1]);
else
{
if(ans[op[i][1]]) continue;
ans[op[i][1]] = max(maxcover,op[i][2]);
}
}
for(int i=1;i<=n;i++)
{
if(a[i] > maxcover && a[i] > ans[i] && ans[i]==0) printf("%d ",a[i]);//考虑未被修改的值!
else if(!ans[i]) printf("%d ",maxcover);
else printf("%d ",ans[i]);
}
}
查询区间中比x小的元素个数
题意:
给出一个序列,每次查询\([L,R]\)区间中\(\le H\)的元素个数。
思路:
原本是主席树板子题,但是有神奇办法线段树可以切掉
考虑线段树同时维护区间的最大最小值,如果一个区间的最大值比\(H\)小,那么整个区间的数计入答案,如果一个区间最小值比\(H\)大,那么这个区间无法计入答案,如果\(H\)介于最大最小之间,那么就按照线段树的分治思想分开递归统计
核心在于线段树的\(query()\)函数怎么写
struct segment_tree{
int maxx[N<<2],minn[N<<2];
#define ls rt<<1
#define rs rt<<1|1
inline void pushup(int rt)
{
maxx[rt] = max(maxx[ls],maxx[rs]);
minn[rt] = min(minn[ls],minn[rs]);
}
inline void build(int rt,int l,int r)
{
if(l==r)
{
maxx[rt] = minn[rt] = a[l];
return;
}
int mid = l+r>>1;
build(ls,l,mid);
build(rs,mid+1,r);
pushup(rt);
}
inline int query(int rt,int l,int r,int L,int R,int val)
{
if(minn[rt] > val) return 0;
if(L<=l && r<=R)
{
if(maxx[rt] <= val) return r-l+1;
}
int ret = 0,mid = l+r>>1;
if(L<=mid) ret += query(ls,l,mid,L,R,val);
if(R> mid) ret += query(rs,mid+1,r,L,R,val);
return ret;
}
}T;
int n,q;
int x,y,z;
void solve()
{
scanf("%d%d",&n,&q);
for(int i=1;i<=n;i++) scanf("%d",a+i);
T.build(1,1,n);
while(q--)
{
scanf("%d%d%d",&x,&y,&z);
printf("%d ",T.query(1,1,n,x,y,z));
}
puts("");
}
(贪心)按位或的最小生成树
题意:
最小生成树,不过是要求边权按位或起来最小
思路:
按位考虑,贪心地,从最高位开始尽量选择边权为0的边
回顾一下,克鲁斯卡尔算法是尽量选择边权最小的边加入边集,但这里的思考我被这个算法给桎梏了。他应该是个边集里删边的算法,我们尽量把高位上为0的边留下来,为1的删除出去,直到形成一棵生成树
具体过程:从高位枚举,再枚举当前边集,如果该条边这一位为0就塞进并查集,最后看联通块是否为1,如果为1说明还可以再继续删且答案的这一位就填0,同时新的边集就是当前边集里这一位为0的边(也就是删边),而如果连通块不为1说明需要一条这一位为1的边,那么就不做删除,由下一位的枚举决定
struct UF{
int fa[N],cnt;
void init()
{
for(int i=1;i<=n;i++) fa[i] = i;
cnt = n;//连通块数
}
int find(int x){return fa[x] = (fa[x] == x) ? x:find(fa[x]); }
void Union(int x,int y)
{
int fx = find(x);
int fy = find(y);
cnt -= (fx!=fy);
fa[fx] = fy;
}
}uf;
struct edge{int u,v,w;};
void solve()
{
scanf("%d%d",&n,&m);
vector<edge> e;
for(int i=1,u,v,w;i<=m;i++)
{
scanf("%d%d%d",&u,&v,&w);
e.push_back({u,v,w});
}
int ans = 0;
for(int i=30;i>=0;i--)
{
uf.init();
vector <edge> ne;//一个边集逐渐缩小的过程
for(auto ed: e)
{
int u = ed.u, v = ed.v, w = ed.w;
if(((w>>i)&1) == 0)
{
ne.push_back(ed);
uf.Union(u,v);
}
}
if(uf.cnt == 1)
{
ans += (0<<i);
e = ne;//新的边集
}
else ans += (1<<i);
}
printf("%d",ans);
}
3.19
(状压)求汉密尔顿回路的方案数
题意:假设两个数互质就是两点有双向边,求从1开始遍历每个点仅一次的方案数
思路:
状态压缩令\(S\)的二进制位01表示到达那个点,如果i位为1就是到过
状压的核心是枚举每种状态,枚举转移关系,然后构造前序状态并转移
\(dp[S][i]\)表示\(S\)状态下当前走到了\(i\)号点,有\(dp[S][i] += dp[S\_from][j]\),其中\(S\_from\)是\(j\)位为\(1\)而\(i\)位为\(0\)的构造出的状态
dp[1][1] = 1;
for(int s=0;s<(1<<21);s++)
{
for(int i=1;i<=21;i++)
{
if((s&(1<<(i-1))) == 0) continue;//s符合i
for(int j=1;j<=21;j++)
{
if((s&(1<<(j-1))) == 0) continue; //s符合j
int s_from = s&(~(1<<(i-1)));//删去i位上的1
if(G[i][j]) dp[s][i] += dp[s_from][j];
}
}
}
long long ans = 0;
for(int i=1;i<=21;i++) ans += dp[(1<<21)-1][i];
(背包)按顺序选数尽可能多选且其之和任意时刻不为负
题意:如题,答案就是最多能选几个数https://codeforces.com/contest/1526/problem/C1
可以类似成可达性背包,背包的值就是为正就是可达,当然要保证做的过程中尽量更大
思路:
\(dp[i][j]\)考虑到前i个数,已经选了j个的最大和,到达不了的先设置为-1,已经选了0个数的设置为0,转移方程 为\(dp[i][j] = max(dp[i-1][j-1]+a[i],dp[i-1][j])\)表示从选\(a[i]\)与不选里面最大的转移过来,当然转移前的位置必须是可达的
可以滚动数组,但是还是先把二维的写熟
memset(dp,-1,sizeof dp);
dp[0][0] = 0;
for(int i=1;i<=n;i++)
{
dp[i][0] = 0;
for(int j=1;j<=i;j++)
{
if(dp[i-1][j-1]+a[i]>=0 && dp[i-1][j-1]>=0) //可达性判断
dp[i][j] = max(dp[i-1][j-1]+a[i],dp[i-1][j]);
else dp[i][j] = dp[i-1][j];
}
}
for(int i=1;i<=n;i++) if(dp[n][i]>=0) ans = i;//答案为最大的可选的个数
3.20
翻转01串改变1的个数的情况数
题意:
给定一个01串,你可以翻转区间\([L,R]\)的数,计算一次操作后,01串中1的总数有多少种
思路:
当时在场上疯狂摸例子但是却没有再深入一步,是经验不足。
考虑一次操作对1的个数的改变值,1变为0,使得1的个数-1,0变为1,则使得1的个数+1
这样我们就把原01串化成一个操作数组:令1用-1代替,0用1代替,对这个数组做子段和就是这个子段里原01串翻转后1增加的个数,那么做最大子段和就是整个串所能增加的最大的1的个数,由于每个值都是加减1,我们有理由相信最后可以取遍 原1的个数 到 最大1的个数 的所有值
对于减少的1的个数,我们把1和-1对换,再做一遍
int solve()//最大子段和
{
int maxx = 0,sum = 0;
for(int i=1;i<=n;i++)
{
sum += a[i];
if(sum < 0) sum = 0;
maxx = max(maxx,sum);
}
return maxx;
}
int ans1,ans2;
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",a+i);
for(int i=1;i<=n;i++)
{
if(a[i] == 1) a[i] = -1;
else a[i] = 1;
}
ans1 = solve();
for(int i=1;i<=n;i++) a[i] = -a[i];
ans2 = solve();
printf("%d",ans1+ans2+1);
return 0;
}
(博弈论)观察操作数的奇偶性
题意:
每次取数列中最大值,把它缩小成另一个数,但要求变化后每个数各不相同。每个数都是非负整数。
思路:
属实看不明白,当搬运工了
考虑最大值\(max\)和次大值\(smax\),为了方便思考,简化成只有他俩的数列,若\(max = smax+1\)则先手必输,若\(max>smax+1\)则先手必定把它改成\(smax+1\)(操作数不为0),此时先手必赢。
如果 \(a[n]>a[n-1]+1\)
如果先手可以让\(a[n]\) 变成小于 \(a[n-1]\)的数,并且这样做可以赢的话,那先手这样做就可以了。
如果让 \(a[n]\)变成小于\(a[n-1]\) 的数不能获胜的话,说明这样必输,先手可以让\(a[n]=a[n-1]+1\) ,这样可以强制让后手执行\(a[n]\)变成小于\(a[n-1]\)的数,后手必输则先手必赢。
如果\(a[n]=a[n-1]+1\)
为了赢取操作空间双方应该会使操作数尽量多,所以我们考虑最大操作数
最后的结果数列一定是\(0,1,2,3......n-1\),每次都操作最大值,所以最大操作数是\(max-(n-1)\)
结合最简模型,最大操作数为奇数先手必赢,偶数后手必赢。