我们刚刚知道那些题的解法-5
Zhengrui 2022ABDay1 B
考虑进行转化,在一个格子内,如果某条边有水管连接,那么就是 \(1\),否则就是 \(0\),所以高级水管等价于相对的两条边的状态相反。
一个状态合法等价于所有边两边标的数都是一样的,最外围的边都认为值标 \(0\)。
若一个格子里填的是高级水管,那么就是好格子,否则就是坏格子。
考虑一行所有竖着的边,如果两个边标的数相同,且之间的边数为奇数,那么里面一定有一个坏格子。如果标的数不同,一样有类似的限制。
不难发现我们仅有这样的限制,也就是说,如果所有限制都满足,那么一定可以构造出合法解。
考虑把所有行限制放在左边,所有列限制放在右边,考虑如果两个限制有交点,那么连一条边。限制的个数确实是 \(n^3\) 个,但是大部分都是没有边相连的,这是因为我们的边数是 \(n^2\) 的。剩下的就是求一个最小边覆盖,跑网络流即可。
没有边相连的点一定有一个 \(1\) 的代价。
不过还有两个限制没有考虑:某个格子必须是好格子以及某个格子我不能填好格子。
首先如果某个格子我不能填好格子的话,那么它一定是没有任何贡献的,我们按照需要填就可以。
至于某个格子必须是好格子这个限制,其实也是好做的,我们只需要关注所有限制是否满足,即是否存在一个限制里面所有的格子都要求是好的。
struct edge{
int to,next,f;
inline void Init(int to_,int ne_,int f_){
to=to_;next=ne_;f=f_;
}
}li[N*N<<1];
int head[N*N*N],now[N*N*N],tail=1,n,m,de[N*N*N],tot,c[N],pl[N][N],pr[N][N];
bool flag=1;
int s,t,d[N*N*N];
char a[N][N];
vi L,R;
inline void Add(int from,int to,int f){
li[++tail].Init(to,head[from],f);head[from]=tail;
swap(from,to);li[++tail].Init(to,head[from],0);head[from]=tail;
}
queue<int> q;
inline bool bfs(){
mset(d,0);
q.push(s);d[s]=1;
while(q.size()){
int top=q.front();q.pop();now[top]=head[top];
Next(top){
int to=li[x].to,f=li[x].f;
if(d[to]||!f) continue;
d[to]=d[top]+1;q.push(to);
}
}
if(d[t]) return 1;return 0;
}
inline int Dinic(int k,int flow){
if(k==t) return flow;
int rest=flow,x;
for(x=now[k];x&&rest;x=li[x].next){
int to=li[x].to,f=li[x].f;
if(d[to]!=d[k]+1||!f) continue;
int re=Dinic(to,min(rest,f));
if(!re) d[to]=-INF;
rest-=re;li[x].f-=re;li[x^1].f+=re;
}
now[k]=x;
return flow-rest;
}
inline int RunDinic(){
int ans=0;
while(bfs()){
ans+=Dinic(s,INF);
}
return ans;
}
inline void chk(int x,int k){
if(c[x]==-1) c[x]=k;else if(c[x]!=k) flag=0;
}
int main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);read(m);
rep(i,1,n){
scanf("%s",a[i]+1);
}
rep(i,1,n){
mset(c,-1);
c[1]=0;c[m+1]=0;
rep(j,1,m){
if(a[i][j]=='1'||a[i][j]=='2') chk(j,1),chk(j+1,0);
else if(a[i][j]=='3'||a[i][j]=='4') chk(j,0),chk(j+1,1);
}
for(int j=1,k=2;j<=m+1&&k<=m+1;j=k++){
while(c[k]==-1) k++;
if(((k-j)&1)==(c[k]^c[j])) continue;
bool ok=0;
rep(o,j,k-1) if(a[i][o]=='x') ok=1;
if(ok) continue;
rep(o,j,k-1) if(a[i][o]=='.') ok=1;
if(!ok){
flag=0;continue;
}
tot++;L.pb(tot);
rep(o,j,k-1) pl[i][o]=tot;
}
}
rep(j,1,m){
mset(c,-1);
rep(i,1,n){
if(a[i][j]=='1'||a[i][j]=='3') chk(i,1),chk(i+1,0);
if(a[i][j]=='2'||a[i][j]=='4') chk(i,0),chk(i+1,1);
}
c[1]=0;c[n+1]=0;
for(int i=1,k=2;i<=n+1&&k<=n+1;i=k++){
while(c[k]==-1) k++;
if(((k-i)&1)==(c[k]^c[i])) continue;
bool ok=0;
rep(o,i,k-1) if(a[o][j]=='x') ok=1;
if(ok) continue;
rep(o,i,k-1) if(a[o][j]=='.') ok=1;
if(!ok){
flag=0;continue;
}
tot++;R.pb(tot);
rep(o,i,k-1) pr[o][j]=tot;
}
}
if(!flag){
puts("-1");return 0;
}
rep(i,1,n)rep(j,1,m){
if(a[i][j]=='.'&&pl[i][j]&&pr[i][j]){
Add(pl[i][j],pr[i][j],1);
de[pl[i][j]]++;
de[pr[i][j]]++;
}
}
s=++tot;t=++tot;
for(int x:L){
if(de[x]) Add(s,x,1);
}
for(int x:R){
if(de[x]) Add(x,t,1);
}
int ans=n*m;
rep(i,1,n)rep(j,1,m){
if(a[i][j]=='x') ans--;
}
int cnt=0;
for(int x:L) if(!de[x]) ans--;else cnt++;
for(int x:R) if(!de[x]) ans--;else cnt++;
ans-=(cnt-RunDinic());
printf("%d\n",ans);
return 0;
}
Zhengrui 2022ABDay1 C
考虑设 \(f_{x,i}\) 表示 \(x\) 子树在 \(i\) 已经选的情况下的最优解,而 \(g_x\) 表示任意情况下 \(x\) 子树的最优解。
考虑转移,显然如果 \(f_{x,i}\) 是从 \(f_{to,i}\) 转移过来,因为 \(i\) 被选的代价减重了,所以要加上 \(s\)。或者我们考虑从 \(g_{to}\) 转移过来。
不难证明最优解的方案一定是包含在其中的。而其余的方案一定都是合法的,这保证了做法的正确性,代码复杂度完虐 std 做法。
#include <bits/stdc++.h>
using namespace std;
long long n,a[1010],t,d,s,dis[1010][1010],f[1010][1010],g[1010];
vector<pair<int,int> >v[1010];
void DFS(int rt,int x,int fa){
for(int i=0;i<v[x].size();i++){
int to=v[x][i].first;
long long val=v[x][i].second;
if(to==fa)continue;
dis[rt][to]=dis[rt][x]+val;
DFS(rt,to,x);
}
}
void dfs(int x,int fa){
for(int i=0;i<v[x].size();i++){
int to=v[x][i].first;
if(to==fa)continue;
dfs(to,x);
}
for(int i=1;i<=n;i++){
f[x][i]=a[x]*(dis[x][i]<=d)-s;
for(int j=0;j<v[x].size();j++){
int to=v[x][j].first;
if(to==fa)continue;
f[x][i]+=max(g[to],f[to][i]+s);
}
}
for(int i=1;i<=n;i++)g[x]=max(g[x],f[x][i]);
}
int main(){
scanf("%lld%lld%lld%lld",&n,&t,&d,&s);
for(int i=1;i<=n;i++)scanf("%lld",&a[i]),a[i]*=t;
for(int i=1;i<n;i++){
int x,y,w;
scanf("%d%d%d",&x,&y,&w);
v[x].push_back({y,w});
v[y].push_back({x,w});
}
for(int i=1;i<=n;++i)DFS(i,i,0);
dfs(1,0);
printf("%lld\n",g[1]);
}
这里由于不想写了,粘了别人的一份代码过来。
考虑 std 做法怎么做。转移其实只需要知道 \(u\) 子树内最浅的被选点是哪个和最深的能被最选点选到的点是哪个即可,考虑两种情况只会出现一种,所以可以转移。
转移是一个树形 DP,不过要我的做法要大分讨。
看了下 maoyiting 的代码,发现其实都可以用背包来转移,全都放在背包里进行转移显然能有效降低代码复杂度。
Zhengrui 2022NOIPDay4 B
感觉思路还是比较自然的。首先考虑是一个排列怎么做,考虑里面 \(n\) 在 \(B\) 序列中的位置在哪里,设为 \(q\),那么首先 \(n\) 在 \(A\) 中的出现位置一定小于等于 \(q\),其次 \(A_1,\cdots,A_q\) 中的数一定会在 \(B_1,\cdots,B_q\) 中出现,因为我们到 \(q\) 一定会把堆中的数清空。于是我们就把整个序列分成了两部分,分别进行计算即可。这提示我们区间 DP。
考虑设 \(f_{l,r,x}\) 表示考虑 \(l\) 到 \(r\) 中小于等于 \(x\) 中出现的数,方案数数多少,枚举 \(x\) 的出现位置即可。注意为了保证不算重,我们只需要关注那些 \(a_k\le x\) 的数即可。
int n,a[N],f[N][N][N],cnt[N],po[N];
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);rep(i,1,n) read(a[i]);
rep(i,1,n) cnt[a[i]]++;
rep(i,1,n) cnt[i]+=cnt[i-1];
dec(i,1,n) a[i]=cnt[a[i]]--;
// rep(i,1,n) printf("%lld ",a[i]);puts("");
rep(i,1,n) po[a[i]]=i;
rep(i,1,n){
rep(j,0,n) f[i][i][j]=1;
}
rep(i,2,n){
rep(j,1,n-i+1){
int l=j,r=j+i-1;
f[l][r][0]=1;
int minn=INF,maxx=-INF;
rep(k,l,r) cmin(minn,a[i]),cmax(maxx,a[i]);
rep(x,1,n){
int posi=po[x];
if(posi<l||posi>r){
f[l][r][x]=f[l][r][x-1];
// printf("f[%d][%d][%d]=%d\n",l,r,x,f[l][r][x]);
continue;
}
if(posi==r){
f[l][r][x]=f[l][r][x-1];
// printf("f[%d][%d][%d]=%d\n",l,r,x,f[l][r][x]);
continue;
}
rep(k,posi,r-1){
if(a[k]>x) continue;
f[l][r][x]=(f[l][r][x]+f[l][k][x-1]*f[k+1][r][x]%mod)%mod;
}
if(a[r]<=x) (f[l][r][x]+=f[l][r][x-1])%=mod;
// printf("f[%d][%d][%d]=%d\n",l,r,x,f[l][r][x]);
}
}
}
int ans=f[1][n][n];
printf("%lld\n",ans);
return 0;
}
CF1004D
由于对称,不妨设 \(x\le \lceil\frac{n}{2}\rceil,y\le \lceil\frac{m}{2}\rceil\),那么矩阵最大值应该就是 \(n+m-x-y\)。首先特判掉 \(n=m\) 且都为奇数,并且 \(x,y\) 都在中间的情况,除去这种情况,其余情况下,第一个被矩阵卡出去的数字一定不是 \(4\) 的倍数,而比它小的数字除了 \(0\) 之外都是 \(4\) 的倍数。由此可以知道 \(x\) 是多少,这样的话可以枚举 \(n,m\),这样 \(n,m,x,y\) 都知道了,直接构造出来矩形,判断即可。
CF677D
设 \(v_t\) 表示所有权值为 \(t\) 的位置组成的集合,我们只需要考虑所有 \(v_{t-1}\) 到 \(v_t\) 的转移即可。有两种转移方式,一是直接在原图上跑多元 bfs,二是对于 \(v_{t}\) 中的每一个数,枚举 \(v_{t-1}\) 中的每个位置,然后转移。
考虑把这两个结合一下:设 \(a=|v_{t-1}|,b=|v_t|\),若 \(ab<nm\),则用第二种转移,否则用第一种转移。
证明复杂度是正确的,考虑 \(ab<nm\),则根据柯西不等式有:
考虑 \(ab>nm\),则有 \(\sum\limits_{i}[ab>nm]nm\),前者需要满足 \(a,b\) 至少一个大于 \(\sqrt{nm}\),因此也是 \(nm\sqrt{nm}\)。
所以复杂度是正确的。
CF contest1737 D
显然把每条边移到 \(1,n\) 中间,且由于操作,每个边都可以移到 \(1,n\) 中间,证明是显然的。所以 floyed 预处理一下即可。
ZR 2354
考虑枚举一下种族显然是可以做到 \(n^3\) 的,如果一个种族出现了 \(cnt\) 次,那么显然我们背包的大小可以限制在 \([-cnt,cnt]\) 内,这样就可以把这个题优化到 \(n^2\)。
#include<bits/stdc++.h>
#define mset(a,b) memset((a),(b),sizeof((a)))
#define rep(i,l,r) for(int i=(l);i<=(r);i++)
#define dec(i,l,r) for(int i=(r);i>=(l);i--)
#define cmax(a,b) (((a)<(b))?(a=b):(a))
#define cmin(a,b) (((a)>(b))?(a=b):(a))
#define Next(k) for(int x=head[k];x;x=li[x].next)
#define vc vector
#define ar array
#define pi pair
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define N 3010
#define M number
using namespace std;
typedef double dd;
typedef long double ld;
typedef long long ll;
typedef unsigned int uint;
typedef unsigned long long ull;
#define int long long
typedef pair<int,int> P;
typedef vector<int> vi;
const int INF=0x3f3f3f3f;
const dd eps=1e-9;
const int mod=998244353;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,c[N],f[N][2*N],cnt[N],v[N],siz[N],m,base,g[2*N],ans;
vi e[N];
inline void dfs(int k,int fat){
siz[k]=1;
rep(i,-m,m) f[k][i+base]=0;
if(v[k]==1) f[k][base+1]=1;
else f[k][base-1]=1;
for(int to:e[k]) if(to!=fat) dfs(to,k);
for(int to:e[k]) if(to!=fat){
rep(i,-m,m) g[i+base]=0;
rep(i,max(-siz[k],-m),min(siz[k],m)){
rep(j,max(-m-i,max(-siz[to],-m)),min(m-i,min(siz[to],m))){
g[i+j+base]=(g[i+j+base]+f[k][i+base]*f[to][j+base]%mod)%mod;
}
}
siz[k]+=siz[to];
rep(i,-m,m) f[k][i+base]=(f[k][i+base]+g[i+base])%mod;
}
// rep(i,-siz[k],siz[k]){
// printf("f[%d][%d]=%d\n",k,i,f[k][i+base]);
// }
rep(i,1,m) ans=(ans+f[k][i+base])%mod;
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
read(n);rep(i,1,n) read(c[i]);
rep(i,1,n-1){
int u,v;read(u);read(v);e[u].pb(v);e[v].pb(u);
}
base=n;
rep(i,1,n) cnt[c[i]]++;
rep(i,1,n){
// printf("i=%d\n",i);
// if(c[i]==i) v[i]=1;
// else v[i]=-1;
rep(j,1,n) if(c[j]==i) v[j]=1;else v[j]=-1;
m=cnt[i];
dfs(1,0);
// printf("m=%d\n",m);
// printf("ans=%d\n",ans);
}
printf("%lld\n",ans);
return 0;
}
ZR 2356
经过观察样例,我们可以考虑通过构造出 \(+1\) 和 \(\times 2\) 两种操作来进行构造。
重点是如何构造出 \(+1\),我们用 \((x,y)\) 来表示那些满足如下条件的 LCS:\(x,y\) 是该 \(LCS\) 在 \(s,t\) 中的最靠前的匹配位置。那么我们如何做到 \(+1\),一个想法是让 \((x-1,y)\) 来代表一个 LCS,\((x,y)\) 来代表剩下的 \(K-1\) 个 LCS,我们要利用 \((x-1,y)\) 来考虑构造出 \(+1\),不妨设 \(s_{x}=s_{x-1}=t_y=1\),那么如下构造可以进行 \(+1\):
\(+1\) 的原因是 \((x-1,y)\) 会在 \(s\) 的末尾剩下一个 \(1\),从而可以有一个 \(100\) 的配对,同样也可以有一个 \(000\) 的配对。而且对于 \((x,y)\) 都会产生 \(000\) 的配对,所以就多出来了一个。
同时,不难发现之前的 \((x-1,y)\) 代表一个,剩下都由 \((x,y)\) 代表的性质不变。
同样,如下构造可以进行 \(\times 2\):
容易发现 LCS 个数确实变成了两倍,只需要考虑是否满足性质即可。不难发现是满足性质的(\(11000\) 的配对。)
ZR 2357
首先可以线段树优化建图跑 spfa,不过没写。
考虑一个朴素的 DP,设 \(f_i\) 表示从 \(i\) 到 \(n\) 的最优值,有一个 naive 的 DP:
考虑 \(\lfloor\frac{j-i}{K}\rfloor=\lfloor\frac{j}{K}\rfloor-\lfloor\frac{i}{K}\rfloor-[j\bmod K<i\bmod K]\),所以 DP 可以写成:
首先一个想法是对于每一个 \(i\bmod K\) 都开一个树状数组,可以做到 \(nK\log n\),不过我们可以用两个树状数组,一个表示前缀 \(f_i-\lfloor\frac{j}{K}\rfloor\times D\) 的最大值,一个表示后缀的最大值,然后转移即可。这里的前缀后缀是在 \(\bmod K\) 的意义下。
复杂度 \(n\log n\)。
ZR 2399
稍微手玩以下就会发现如果一个 \(n\time m\) 的矩形,被填了一个 \(a\times b\) 的左上角,且 \(a<n,b<m\),发现这样是无法继续往下填的,也就是说,我们考虑每次填一个矩形 \(a\times b\) 必须要满足 \(a=n\) 或者 \(b=m\)。
不妨假设 \(n<m\),经过构造后可以分别知道 \(n\) 为 \(1,2\),是奇数,是偶数时能填的矩形,往上填即可。
经过讨论不难发现不用再计算 \(m,n\)。
下面放一张讨论结果。
ZR 2400
观察可知 \(n\) 很大的时候 \(d\) 不会很大,因为有值域的限制,经过一些估算可以知道 \(nd\le 2\times 10^6\),不过我们可以直接枚举 \(d\),写个卡时就行。这是小问题。
枚举 \(d\) 之后考虑怎么做,题目要求两个等差数列,考虑一个怎么做,设 \(b_i=a_i-(i-1)d\),然后假设首项为 \(x\),那么答案应该就是:
注意第二个 \(\sum\) 会有一些奇偶性的问题,我们先不去管它,发现整个式子相当于一个带权中位数,那么在把所有的 \(c\) 排序之后我们应该选第 \(n-\lceil\frac{n}{3}\rceil+1\) 这个位置的 \(c\),注意这里相当于是取到最小值的最右边的 \(c\),所以考虑上奇偶性就在考虑一下左边的那个 \(c\) 值减一即可。
两个等差数列相当于一个前缀和一个后缀,考虑预处理之后枚举断点,预处理的时候需要维护中位数,除了线段树,还可以用常数更小的对顶堆来实现,只需要同时维护一个堆的奇数和,奇数个数,偶数和,偶数个数,另一个堆的和和个数就可以了。
详细看代码。
#include<bits/stdc++.h>
#define mset(a,b) memset((a),(b),sizeof((a)))
#define rep(i,l,r) for(int i=(l);i<=(r);i++)
#define dec(i,l,r) for(int i=(r);i>=(l);i--)
#define cmax(a,b) (((a)<(b))?(a=b):(a))
#define cmin(a,b) (((a)>(b))?(a=b):(a))
#define Next(k) for(int x=head[k];x;x=li[x].next)
#define vc vector
#define ar array
#define pi pair
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define N 100010
#define M number
using namespace std;
typedef double dd;
typedef long double ld;
typedef long long ll;
typedef unsigned int uint;
typedef unsigned long long ull;
#define int long long
typedef pair<int,int> P;
typedef vector<int> vi;
const int INF=1e18;
const dd eps=1e-9;
template<typename T> inline void read(T &x) {
x=0; int f=1;
char c=getchar();
for(;!isdigit(c);c=getchar()) if(c == '-') f=-f;
for(;isdigit(c);c=getchar()) x=x*10+c-'0';
x*=f;
}
int n,a[N],ans,pr[N],su[N],b[N];
int sum,cnt[2],sm[2],cn;
priority_queue<int> q1;
priority_queue<int,vc<int>,greater<int> > q2;
inline void push_1(int x){
cnt[x&1]++;
sm[x&1]+=x;
q1.push(x);
}
inline void push_2(int x){
sum+=x;
cn++;
q2.push(x);
}
inline int pop_1(){
int x=q1.top();
cnt[x&1]--;
sm[x&1]-=x;
q1.pop();
return x;
}
inline int pop_2(){
int x=q2.top();
sum-=x;
cn--;
q2.pop();
return x;
}
inline int get_ans(int x){
int o=x&1;
int nowans=(x*cnt[o]-sm[o])/2+(x*cnt[o^1]-sm[o^1]+cnt[o^1])/2+cnt[o^1]+sum-cn*x;
// printf("x=%d nownas=%lld\n",x,nowans);
return nowans;
}
inline void solve(int *f){
while(q1.size()) q1.pop();
while(q2.size()) q2.pop();
sm[0]=sm[1]=cnt[0]=cnt[1]=sum=0;cn=0;
f[0]=0;
rep(i,1,n){
// printf("i=%d\n",i);
if(q2.size()==0){
push_2(b[i]);
int k=q2.top();
f[i]=min(get_ans(k),get_ans(k-1));
continue;
}
int to=q2.top();
if(to<=b[i]) push_2(b[i]);
else push_1(b[i]);
if(q1.size()<i-(i+2)/3){
assert(q2.size());
push_1(pop_2());
}
if(q1.size()>i-(i+2)/3){
assert(q1.size());
push_2(pop_1());
}
int k=q2.top();
f[i]=min(get_ans(k),get_ans(k-1));
}
}
inline void calc(int d){
// printf("d=%d\n",d);
rep(i,1,n) b[i]=a[i]-(i-1)*d;
// rep(i,1,n){
// printf("b[%d]=%d\n",i,b[i]);
// }
solve(pr);reverse(b+1,b+n+1);solve(su);
rep(i,0,n){
ans=min(ans,pr[i]+su[n-i]);
// printf("pr[%d]=%d su[%d]=%d\n",i,pr[i],n-i,su[n-i]);
}
}
signed main(){
// freopen("my.in","r",stdin);
// freopen("my.out","w",stdout);
ans=INF;
read(n);rep(i,1,n) read(a[i]);
for(int i=1;clock()<=0.8*CLOCKS_PER_SEC;i++) calc(i),calc(-i);
// calc(3);
printf("%lld\n",ans);
return 0;
}