0716 题解
T1
\(S\)本身就有的我们先剔除,只用考虑\(T\)之间左右相邻的和\(S,T\)之间上下相邻
\(O(n^2)\)的暴力\(dp\)似乎很显,这里当然也可以\(bitset\)优化一下
具体的就是\(dp_{i,j,0}\)为第\(i\)位填\(0\)时有\(j\)对不同的是否可行,\(dp_{i,j,1}\)同理
我们这里可以考虑探究一下固定\(i\),\(j\)之间是否有性质
我们设\(dp_{i,0}\)为满足的\(j\)构成的集合
结论:\(dp_{i,0}\)和\(dp_{i,1}\)均是一个区间但有可能中间被挖去一个值\(v\)
这个可以打表找出来吧。。
注意到\(dp_{i,0},dp_{i,1}\)是一直重叠的
考虑归纳证明,边界就不管了,\(i=2\)的情况可以下去自己手算一下,\(i=1\)的情况可以先不管
对于\(i\),这里我们可以先观察一下转移,可以发现区间取并时只用关注\(0/1\)的相对位置,随便一个平移一下就行了,抽象为
\(dp_{i,0}=dp_{i-1,0}\cup(dp_{i-1,1}+1)\)
\(dp_{i,1}=dp_{i-1,1}\cup(dp_{i-1,0}+1)+(\pm1)\)
这里就只先解决这种情况,其他的同理,\(0/1\)整体平移一下即可
首先对于\(x\in dp_{i-1,0},dp_{i-1,1}\)
可以发现\(dp_{i,0}\)产生的是\(\{x,x+1\}\),\(dp_{i,1}\)产生的是\(\{x+1,x+2\}\)或者\(\{x,x-1\}\)依旧有重叠部分
所以\(0/1\)始终有重叠
那这里缺的位置是只有原有区间来产生,不会因为并产生空
可以假定这里缺的位置不能是端点,因为是端点一定能被填上或者可以忽略
那么对于缺的位置还有一个更强的结论,对于一个\(0\)缺的位置\(v\),\(v+1\)或\(v-1\)一定会出现在\(1\)中
当然换一下也是成立的
这里同样归纳证明
如果\(v+1\)出现在\((i-1,1)\)中,那\((0,i)\)就缺\(v\),\((1,i)\)什么都不缺
如果\(v-1\)出现在\((i-1,1)\)中,那\((0,i)\)什么都不缺,\((1,i)\)缺\(v/v+2\),但\(v+2\)还是\(v\)在\((0,i)\)中都有\(v+1\)相邻
由此,如果\(i=2\)只有一个缺口,每次操作后最多也只会有一个缺口
最后直接\(O(n)\)转移即可
T2 nk
\(source:\)https://atcoder.jp/contests/arc102/tasks/arc102_d
\(subtask1\):
也许有人会写搜索?
期望得分:\(20 pts\)
\(subtask2\):
显然结果与每次操作顺序无关
考虑每次暴力遍历整个数组,若当前点可以操作则直接进行,直到序列有序。
序列有序等价于逆序对为\(0\),每次操作从\(p_i>p_{i+1}>p_{i+2}\)变为\(p_i<p_{i+1}<p_{i+2}\),逆序对减少了\(3\),而初始逆序对是在\(n^2\)级别的,所以总操作次数也在\(n^2\)级别,再加上最坏每次寻找操作点遍历整个数组,时间复杂度上界在\(O(n^3)\)左右。但我不知道为什么n<2000也能跑得飞快
期望得分:\(50pts\)
\(subtask3\):
妙妙结论题。
首先每次操作时每个数所在下标的奇偶性不变,因此初始一个数下标和值奇偶性不同那么一定无解。
再考虑逆序对,首先由\(subtask2\)的复杂度分析有初始序列的逆序对数一定是3的倍数才可能有解。
尝试将逆序对的结论进行扩展,可以发现每次操作后下标为奇数或偶数的数构成的序列逆序对数量都减少了1。也就是说,每次整个序列逆序对减少3一定伴随着下标为奇数或偶数之一的数构成的序列逆序对数量减1。
因此我们可以得到有解的一个必要条件:初始下标为奇数和偶数的数分别构成的两个序列的逆序对之和是原序列的\(\frac{1}{3}\)。
显然这个条件也是充分的。
直接树状数组/归并排序求逆序对即可,时间复杂度\(O(n\log n)\)
期望得分:\(100pts\)
\(PS\):本题似乎存在\(O(n)\)做法,但是搬题人太蔡了不会。
T3 ds
某两位同学看到过这道题,其中某位同学写了这道题,愿他们都能场切这道题
\(source\):
https://www.luogu.com.cn/problem/P3863
https://www.luogu.com.cn/problem/P5356
\(subtask1\):
暴力模拟即可,时间复杂度\(O(mn\log n)\) 或\(O(mn)\),空间复杂度\(O(mn)\)
期望得分:\(20pts\)
\(subtask2\):
暴力时间复杂度还是可以通过,但是会被卡空间,考虑优化暴力记录数组的每个历史版本这一过程。
离线可以维护一个位置在\([0,m]\)时间内的信息。
具体来说,我们可以将询问和修改离线下来,把修改差分,拆成两个后缀的修改,分别为\((l,x,t)\),\((r+1,-x,t)\),其中\((pos,val,tim)\)表示在时间为\(t\)时将\([pos,n]\)全部加上\(val\)。
将询问和修改按照第一维位置升序,第二维时间升序排序。
维护一个修改位置的指针,依次升序扫过每个询问,对于当前指针指向的修改如果位置更靠前,或位置一样但修改时间靠前,则更新修改\((t,m)\)这一段并向右挪动指针。然后就可以对当前询问计算答案。
考虑算法正确性,发现因为修改影响的是一段后缀的时间和位置,那么排在这个询问后的修改一定对当前的答案没有影响。本质是扫描线。
说得有点抽象,放核心代码。
void solve(){
sort(c+1,c+1+ccnt);sort(q+1,q+1+qcnt);
int now=1;
for (int i=1;i<=qcnt;i++){
while(((c[now].pos<q[i].pos)||(c[now].pos==q[i].pos&&c[now].tim<q[i].tim))&&now<=ccnt){
update(c[now].tim,m,c[now].x);
++now;
}
ans[q[i].id]=kth(q[i].l,q[i].r,q[i].x);
}
}
如果暴力维护区间加,区间第\(k\)小,时间复杂度不变,空间复杂度变为\(O(n)\)
期望得分:\(40pts\)
\(subtask3\):
你都会sub2了还不会sub3?
教主的魔法都做过吧?
区间加,区间大于等于一个给定数的数的个数
分块维护块内排序后序列,询问时散块暴力,整块二分
那么区间第\(k\)小可以由教主的魔法的询问再套上一个二分得到
时间复杂度\(O(m\sqrt{m\log m } \log m)\)
点击查看代码
#include <bits/stdc++.h>
#define il inline
using namespace std;
const int Maxn = 1e5 + 5;
const int B = 300;
const int Inf = 2e9;
bool Beg;
int n, m;
struct Dat1 {int l, r, k;};
vector <Dat1> T[Maxn];
struct Dat2 {int l, r, k, id;};
vector <Dat2> C[Maxn];
int tot, ans[Maxn];
int a[Maxn];
vector <int> V[Maxn];
int bg[Maxn], ed[Maxn], bel[Maxn], tag[Maxn];
il void init() {
int num = m / B;
ed[0] = -1;
for(int i = 1; i <= num; i++) {
bg[i] = ed[i - 1] + 1;
ed[i] = bg[i] + B - 1;
}
ed[num] = m;
for(int i = 1; i <= num; i++) {
for(int j = bg[i]; j <= ed[i]; j++) bel[j] = i, V[i].push_back(a[j]);
}
}
il void rebuild(int p) {
V[p].clear();
for(int i = bg[p]; i <= ed[p]; i++) V[p].push_back(a[i]);
sort(V[p].begin(), V[p].end());
}
il void Mdf(int l, int r, int x) {
int L = bel[l], R = bel[r];
if(L == R) {
for(int i = l; i <= r; i++) a[i] += x;
rebuild(L);
}
else {
for(int i = l; i <= ed[L]; i++) a[i] += x;
rebuild(L);
for(int i = L + 1; i < R; i++) tag[i] += x;
for(int i = bg[R]; i <= r; i++) a[i] += x;
rebuild(R);
}
}
il int query(int l, int r, int mid) {
int L = bel[l], R = bel[r], sum = 0;
if(L == R) {
for(int i = l; i <= r; i++) if(a[i] + tag[L] <= mid) sum++;
}
else {
for(int i = l; i <= ed[L]; i++) if(a[i] + tag[L] <= mid) sum++;
for(int i = L + 1; i < R; i++) {
int num = mid - tag[i];
sum += upper_bound(V[i].begin(), V[i].end(), num) - V[i].begin();
}
for(int i = bg[R]; i <= r; i++) if(a[i] + tag[R] <= mid) sum++;
}
return sum;
}
il int qmin(int l, int r) {
int L = bel[l], R = bel[r], mn = Inf;
if(L == R) {
for(int i = l; i <= r; i++) chkmin(mn, a[i] + tag[L]);
}
else {
for(int i = l; i <= ed[L]; i++) chkmin(mn, a[i] + tag[L]);
for(int i = L + 1; i < R; i++) chkmin(mn, V[i][0] + tag[i]);
for(int i = bg[R]; i <= r; i++) chkmin(mn, a[i] + tag[R]);
}
return mn;
}
il int qmax(int l, int r) {
int L = bel[l], R = bel[r], mx = -Inf;
if(L == R) {
for(int i = l; i <= r; i++) chkmax(mx, a[i] + tag[L]);
}
else {
for(int i = l; i <= ed[L]; i++) chkmax(mx, a[i] + tag[L]);
for(int i = L + 1; i < R; i++) chkmax(mx, V[i].back() + tag[i]);
for(int i = bg[R]; i <= r; i++) chkmax(mx, a[i] + tag[R]);
}
return mx;
}
il int Query(int pl, int pr, int k) {
int l = qmin(pl, pr), r = qmax(pl, pr), res = 0;
while(l <= r) {
int mid = (l + r) >> 1;
if(query(pl, pr, mid) >= k) res = mid, r = mid - 1;
else l = mid + 1;
}
return res;
}
bool End;
il void Usd() {cerr << (&Beg - &End) / 1024.0 / 1024.0 << "MB " << (double)clock() * 1000.0 / CLOCKS_PER_SEC << "ms\n"; }
int main() {
// freopen("ds4.in", "r", stdin);
// freopen("my.out", "w", stdout);
read(n), read(m);
for(int i = 1; i <= m; i++) {
int opt, l, r, p, k;
read(opt);
switch(opt) {
case 0: {
read(l), read(r), read(k);
T[l].push_back({i, m, k}), T[r + 1].push_back({i, m, -k});
break;
}
case 1: {
read(p), read(l), read(r), read(k);
C[p].push_back({l, r, k, ++tot});
break;
}
}
}
init();
for(int i = 1; i <= n; i++) {
for(auto p : T[i]) Mdf(p.l, p.r, p.k);
for(auto p : C[i]) ans[p.id] = Query(p.l, p.r, p.k);
}
for(int i = 1; i <= tot; i++) {
write(ans[i]);
}
Usd();
return 0;
}
//感谢dzb大佬的码TT
期望得分:\(100pts\)
T4
分析一下问题,可以发现瓶颈在于限制牛仔序列
前\(20pts\)似乎直接状压+子序列DP即可
\(sub3\)是个很好的启发,由于\(A\)就是牛仔序列,我们只需要统计每个\(A\)的贡献
直接枚举\(A\)的位置算方案数,答案是\((n-m+1)k^{n-m}\)
\(sub4\)我们先剔除\(m=n\)的情况
剩下的,考虑容斥,先计算所有\(A\)可能的贡献\((n-m+1)k^{n-m}\),最后减去没有牛仔序列的序列中\(A\)的贡献
注意到没有牛仔序列等价于不存在连续的长度为\(K\)元素不同的段
同样考虑子序列\(dp\)
设\(dp_{i,j}\)表示填前\(i\)个数后末尾有长度为\(j\)连续的元素不同的段的方案数
转移的话考虑
\(j\rightarrow j+1\),即填上前面\(j\)个没出现的数,转移系数为\(k-j\)
\(j\rightarrow p,(p\le j)\),即填上前面出现的数,根据与末尾的位置决定\(p\)
这里直接后缀和优化即可,这里我们只需限制\(j<k\)即可保证无牛仔序列
注意这里计算的是牛度,我们可以发现每个元素均是等价的,所以我们可以计算所有长度为\(m\)元素不同的段的贡献最后除\(\dfrac{fac_k}{fac_{k-m}}\),对于这个的计算,可以记录一个辅助数组表示牛度,转移是一样的,就是最后\(j\ge m\)时要算上自己的贡献
其实这里离正解就不远了,可以发现就只剩一种情况:\(A\)中有相同元素
如果\(A\)有相同元素,一个合法的牛仔序列一定不会横跨\(A\),因此我们可以考虑直接枚举\(A\)的匹配位置算方案数
可以发现这个可以借助上面的\(dp\)的定义和转移,唯一的区别在于初值不同
时间复杂度\(O(nk)\)
点击查看代码
#include<bits/stdc++.h>
#define ll long long
#define il inline
using namespace std;
namespace asbt{
namespace cplx{bool begin;}
const int maxn=2e5+5,B=450;
int n,m;
ll a[maxn],b[505][505],sb[505][505];
struct{
int bn,L[505],R[505],bel[maxn];
ll d[maxn],sd[505],di[maxn],sdi[505];
il void build(){
bn=(n+B-1)/B;
for(int i=1;i<=bn;i++){
L[i]=R[i-1]+1;
R[i]=min(R[i-1]+B,n);
for(int j=L[i];j<=R[i];j++){
bel[j]=i;
}
}
}
il void add(int p,ll x){
if(p>n){
return ;
}
d[p]+=x,sd[bel[p]]+=x;
di[p]+=x*p,sdi[bel[p]]+=x*p;
}
il ll queryd(int l,int r){
if(l>r){
return 0;
}
int pl=bel[l],pr=bel[r];
ll res=0;
if(pl==pr){
for(int i=l;i<=r;i++){
res+=d[i];
}
}
else{
for(int i=l;i<=R[pl];i++){
res+=d[i];
}
for(int i=L[pr];i<=r;i++){
res+=d[i];
}
for(int i=pl+1;i<pr;i++){
res+=sd[i];
}
}
return res;
}
il ll querydi(int l,int r){
if(l>r){
return 0;
}
int pl=bel[l],pr=bel[r];
ll res=0;
if(pl==pr){
for(int i=l;i<=r;i++){
res+=di[i];
}
}
else{
for(int i=l;i<=R[pl];i++){
res+=di[i];
}
for(int i=L[pr];i<=r;i++){
res+=di[i];
}
for(int i=pl+1;i<pr;i++){
res+=sdi[i];
}
}
return res;
}
}BL;
namespace cplx{
bool end;
il double usdmem(){return (&begin-&end)/1048576.0;}
}
int main(){
// cout<<cplx::usdmem();
ios::sync_with_stdio(0),cin.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++){
cin>>a[i];
a[i]+=a[i-1];
}
BL.build();
while(m--){
int opt;
cin>>opt;
if(opt==1){
int x,y;
ll k;
cin>>x>>y>>k;
y=min(y,x-1);
if(x<=B){
for(int i=0;i<=y;i++){
b[x][i]+=k;
}
sb[x][0]=b[x][0];
for(int i=1;i<x;i++){
sb[x][i]=sb[x][i-1]+b[x][i];
}
}
else{
for(int i=1;i<=n;i+=x){
BL.add(i,k),BL.add(i+y+1,-k);
}
}
}
else{
int l,r;
cin>>l>>r;
ll ans=a[r]-a[l-1];
for(int i=1;i<=B;i++){
int zl=(l-1)/i,zr=(r-1)/i;
int sl=(l-1)%i,sr=(r-1)%i;
if(zl==zr){
ans+=sb[i][sr]-(sl?sb[i][sl-1]:0);
}
else{
ans+=sb[i][i-1]-(sl?sb[i][sl-1]:0)+sb[i][sr];
ans+=sb[i][i-1]*(zr-zl-1);
}
}
ans+=(r-l+1)*BL.queryd(1,l-1);
ans+=(r+1)*BL.queryd(l,r);
ans-=BL.querydi(l,r);
cout<<ans<<"\n";
}
}
return 0;
}
}
int main(){return asbt::main();}