T1:P1725 琪露諾
簡單 \(\text{dp}\) 題,狀態轉移很好寫:\(f_i=\max_{j=i-R}^{i-L}f_j+a_i\)。
暴力做是 \(O(n^2)\) 的,考慮最佳化。
發現每次查詢一個區間內的 \(f\) 最大值,直接上線段樹即可,時間複雜度 \(O(nlogn)\)。
還可以單調佇列最佳化,時間複雜度線性。
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=4e5+10,INF=2e9;
int n,L,R,a[N],f[N],mx[N<<2];
void pushup(int u){mx[u]=max(mx[u<<1],mx[u<<1|1]);}
void modify(int u,int l,int r,int x,int v){
if(l==r){mx[u]=v;return;}
int mid=(l+r)>>1;
if(x<=mid) modify(u<<1,l,mid,x,v);
else modify(u<<1|1,mid+1,r,x,v);
pushup(u);
}
int query(int u,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return mx[u];
int mid=(l+r)>>1,ans=-INF;
if(ql<=mid) ans=max(ans,query(u<<1,l,mid,ql,qr));
if(qr>mid) ans=max(ans,query(u<<1|1,mid+1,r,ql,qr));
return ans;
}
int main(){
n=rd,L=rd,R=rd;n++;
FOR(i,1,n) a[i]=rd;
memset(mx,-0x3f,sizeof(f)),f[1]=0;
modify(1,1,2*n,1,0);
FOR(i,1,n+R){
if(i-L<=0) continue;
f[i]=max(f[i],query(1,1,2*n,max(i-R,1),i-L)+a[i]);
modify(1,1,2*n,i,f[i]);
}
int ans=-INF;
FOR(i,n,n+R) ans=max(ans,f[i]);
printf("%d\n",ans);
return 0;
}
T2:P8346 「Wdoi-6」最澄澈的空與海
顯然,一張二分圖若要有恰好 \(1\) 個完美匹配,必須滿足存在入度為 \(1\) 的節點。
但這還不夠,上面這倆不是充要條件,反例很容易舉出。
發現入度為 \(1\) 的點是很重要的,從它入手縮小問題規模。
對於入度為 \(1\) 的點,我們可以找到它對面與它唯一相連的節點,此時這兩個點被鎖死了。所以它對面的那個點,除了和它相連的
邊,其他都無用了,可以刪去。
所以我們要維護所有入度為 \(1\) 的點,模擬上述操作。只要最終所有點刪完,即二分圖恰有 \(1\) 個完美匹配。
可以用拓撲排序的思想來做,儲存入度為 \(1\) 的點。
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
#define debug cout<<"ILOVECCF!"<<endl;
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=4e6+10;
int T,n,m,d[N],del[N];
vector<int> e[N];
bool toposort(){
memset(del,0,sizeof(del));
queue<int> q;int cnt=0;
FOR(i,1,2*n) if(d[i]==1) q.push(i);
while(!q.empty()){
int t=q.front();q.pop();
if(del[t]) continue;del[t]=1,cnt++;
int v;
for(auto y:e[t]){
if(!del[y]){v=y;break;}
}
if(!v) return false;
del[v]=1,cnt++;
for(auto y:e[v]){
if(!del[y]&&--d[y]==1) q.push(y);
}
}
return cnt==2*n;
}
int main(){
T=rd;
while(T--){
FOR(i,1,2*n) e[i].clear();memset(d,0,sizeof(d));
n=rd,m=rd;
FOR(i,1,m){
int x=rd,y=rd;y+=n;
d[x]++,d[y]++;
e[x].push_back(y),e[y].push_back(x);
}
if(toposort()) puts("Renko");
else puts("Merry");
}
return 0;
}
T3:P4514 上帝造題的七分鐘
給你一個矩陣,子矩陣加、求子矩陣和,二維樹狀陣列模版題。
類比一維樹狀陣列維護區間加、區間和的思路即可,計算每個位置的出現次數。
最後需要開 \(4\) 個樹狀陣列,區間加就用差分的思想,區間求和就用字首和思想。
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+ch-'0',ch=getchar();
return x*f;
}
const int N=3000;
int n,m,q;ll tr1[N][N],tr2[N][N],tr3[N][N],tr4[N][N];
int lowbit(int x){return x&-x;}
void add(int x,int y,int v,ll tr[][N]){
for(int i=x;i<=n;i+=lowbit(i))
for(int j=y;j<=m;j+=lowbit(j))
tr[i][j]+=v;
}
ll query(int x,int y,ll tr[][N]){
ll res=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j))
res+=tr[i][j];
return res;
}
void modify(int x,int y,int v){
add(x,y,v,tr1),add(x,y,v*x,tr2),add(x,y,v*y,tr3),add(x,y,v*x*y,tr4);
}
ll getsum(int x,int y){
return (x+1)*(y+1)*query(x,y,tr1)-(y+1)*query(x,y,tr2)-(x+1)*query(x,y,tr3)+query(x,y,tr4);
}
int main(){
char ch;cin>>ch;n=rd,m=rd;
while(cin>>ch){
int a=rd,b=rd,c=rd,d=rd,v;
if(ch=='k'){
v=rd;
modify(a,b,v),modify(c+1,d+1,v),modify(c+1,b,-v),modify(a,d+1,-v);
}
else{
printf("%lld\n",getsum(c,d)-getsum(a-1,d)-getsum(c,b-1)+getsum(a-1,b-1));
}
}
return 0;
}
T4:P7838 「Wdoi-3」夜雀 treating
貪心+線段樹
首先模擬一下題中所述過程,發現是先把序列中點選出來,然後它的左右兩邊劃分為兩個數列,分別設為 \(A,B\)。
接下來我們每次操作,選出一個數列最後的一個數,然後再另一個數列中刪去一個數。
考慮暴力去做,可以先列舉答案區間,然後我們選擇在區間的數,刪去不在區間的最末端的數。這樣做是最優的。
時間複雜度 \(O(n^3)\),考慮最佳化。
容易發現,對於區間 \([l,r]\),如果它裡面的數能被滿足,那麼 \([l+1,r],[l,r-1]\) 也能滿足。對於這樣的經典性質,可以直接雙指標最佳化,時間複雜度降為 \(O(n^2)\)。
還要繼續最佳化,我們發現每次列舉答案區間,都要暴力的去模擬一遍上述過程,這是不可接受的。但發現每次移動區間左右端點,只會令某個數是否選擇發生改變。
假如要選 \(A\) 中的數,發現 \(B\) 的所有字尾中,不在區間內的數必須小於等於相應的 \(A\) 的字尾內在區間內的數。
所以對於每個位置,維護 \(A\) 字尾與 \(B\) 字尾的差,滿足全域性最小值 \(<0\),則區間滿足條件。
直接上線段樹就好了,區間修改,全域性最小值。
時間複雜度 \(O(nlogn)\)。
點選檢視程式碼
#include<bits/stdc++.h>
using namespace std;
#define rd read()
#define ll long long
#define FOR(i,j,k) for(int i=j;i<=k;i++)
#define ROF(i,j,k) for(int i=j;i>=k;i--)
inline int read(){
int x=0,f=1;char ch=getchar();
while(!isdigit(ch)){if(ch=='-')f=-1;ch=getchar();}
while(isdigit(ch)) x=(x<<3)+(x<<1)+(ch^48),ch=getchar();
return x*f;
}
const int N=4e5+10;
int n,a[N],pos[N],mn[N<<2],tag[N<<2];
void pushup(int u){mn[u]=min(mn[u<<1],mn[u<<1|1]);}
void pushdown(int u){
tag[u<<1]+=tag[u],tag[u<<1|1]+=tag[u];
mn[u<<1]+=tag[u],mn[u<<1|1]+=tag[u];
tag[u]=0;
}
void build(int u,int l,int r){
if(l==r){mn[u]=l;return;}
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r),pushup(u);
}
void modify(int u,int l,int r,int ql,int qr,int v){
if(ql<=l&&r<=qr){mn[u]+=v,tag[u]+=v;return;}
pushdown(u);int mid=(l+r)>>1;
if(ql<=mid) modify(u<<1,l,mid,ql,qr,v);
if(qr>mid) modify(u<<1|1,mid+1,r,ql,qr,v);
pushup(u);
}
int main(){
n=rd;FOR(i,1,2*n+1) a[i]=rd,pos[a[i]]=i;
int l=1,r=0,ans=0;build(1,1,n);
while(r<=2*n+1){
if(mn[1]<0){
l++;
if(pos[l-1]==n+1) continue;
if(pos[l-1]<n+1) modify(1,1,n,pos[l-1],n,1);
else modify(1,1,n,2*n+2-pos[l-1],n,1);
}
else{
ans=max(ans,r-l+1),r++;
if(pos[r]==n+1) continue;
if(pos[r]<n+1) modify(1,1,n,pos[r],n,-1);
else modify(1,1,n,2*n+2-pos[r],n,-1);
}
}
printf("%d\n",ans);
return 0;
}