2024-03-14

2024-03-14

Riddle

继续做上次没做出来的题

2-SAT
限制是

  • 如果一个点不选,那么与它相连的所有点都必须选
  • 如果一个点选了,那么和他在同一个部分的所有点都不能选

对于边的限制直接建
但是“部分”的限制直接建图是 \(O(n^2)\)

优化方法是 前缀优化建图
对于每一个部分,用 \(a_i\) 表示这一个部分的第 \(i\) 个点
\(p_{a_i}\) 表示 \(a_i\) 这个点之前的点有没有被选上的

那么直接按下面的方式建边即可

\[a_i \longrightarrow p_{a_i} \ \ \ \ \ {p_{a_i}}^{\prime} \longrightarrow {a_i}^{\prime} \ \ \ \ \ p_{a_{i-1}} \longrightarrow {a_i}^{\prime} \ \ \ \ \ a_i \longrightarrow {p_{a_{i-1}}}^{\prime} \ \ \ \ \ p_{a_{i-1}} \longrightarrow p_{a_i} \ \ \ \ \ {p_{a_i}}^{\prime} \longrightarrow {p_{a_{i-1}}}^{\prime} \]

这样的建图和原来等价

#include<iostream>
#include<cstring>
#include<algorithm>

#define v1(x) (x)
#define v0(x) (x+n)
#define p1(x) (x+n+n)
#define p0(x) (x+n+n+n)

using namespace std;

const int N=1e6+100;
const int V=4*N,E=2*V;

int n,m,k;
int w,a[N];

int hd[V],edg[E],nxt[E],idx;

void adde(int u,int v) {
	edg[idx]=v,nxt[idx]=hd[u],hd[u]=idx++;
}

int grp[V],cnt;
int dfn[V],low[V],idt;
int stk[V],top;
bool st[V];

void Tarjan(int u) {
    dfn[u]=low[u]=++idt;
    stk[++top]=u,st[u]=true;
    for(int i=hd[u];~i;i=nxt[i]) {
        int v=edg[i];
        if(!dfn[v]) {
            Tarjan(v);
            low[u]=min(low[u],low[v]);
        }
        else if(st[v]) low[u]=min(low[u],dfn[v]);
    }
    if(low[u]==dfn[u]) {
        int v;
        cnt++;
        do {
            v=stk[top--];
            st[v]=false;
            grp[v]=cnt;
        }while(v!=u);
    }
}

int main() {
	memset(hd,-1,sizeof(hd));
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=m;i++) {
		int u,v;
		scanf("%d%d",&u,&v);
		adde(v0(u),v1(v)),adde(v0(v),v1(u));
	}
	for(int i=1;i<=k;i++) {
		scanf("%d",&w);
		for(int j=1;j<=w;j++) scanf("%d",&a[j]);
		for(int j=1;j<=w;j++) {
			adde(v1(a[j]),p1(a[j])),adde(p0(a[j]),v0(a[j]));
			if(j>1) {
				adde(p1(a[j-1]),p1(a[j])),adde(p0(a[j]),p0(a[j-1]));
				adde(p1(a[j-1]),v0(a[j])),adde(v1(a[j]),p0(a[j-1]));
			}
		}
	}
	for(int i=1;i<=4*n;i++) if(!dfn[i]) Tarjan(i);
	string ans="TAK";
	for(int i=1;i<=n;i++) {
		if(grp[v0(i)]==grp[v1(i)]) {
			ans="NIE";
			break;
		}
	}
	cout<<ans<<endl;
	
	return 0;
}

整体二分

复习整体二分

整体二分用于处理

  • 多次询问 可以离线
  • 答案可以二分

我们可以对于每个询问都猜测它的答案是 mid 然后分为 不大于 mid 和 大于 mid 两个部分继续分治 l==r 时结束这个部分的分治

引用题解的一段话理解

显然可以给每个询问二分答案,统计该询问中小于等于mid的元素个数。如果大于等于k,说明猜大了,否则说明猜小了。

如果用这种方法的话,对于每个询问都至少要用O(询问矩阵大小*log值域)的时间复杂度解决,多组询问的话时间不能接受。

发现多个询问的二分答案是可以同时被检验的,我们可以为所有询问同时二分答案,把所有答案小于等于mid的询问放在询问序列的左侧,大于mid的放到询问序列的右侧然后递归处理。

Dynamic Rankings

板子题:区间带修改第 K 大

不贴代码占地了

矩阵乘法

# 题目描述 #
给你一个 \(n×n\) 的矩阵,不用算矩阵乘法,但是每次询问一个子矩形的第 \(k\) 小数。

把树状数组改成二维的
把值小于 mid 的所有点涂黑,就能查询每个询问的子矩阵里面小于等于 mid 的点的个数了
因为要清空,所以记录一个 cur 数组,每次如果这个询问要被分到右边的话就更新一下

树状数组不要写 while 循环了,不知道怎么就寄了。。改成for循环就对了

for(int i=x;i<=n;i+=lowbit(i))
	for(int j=y;j<=n;j+=lowbit(j))
		//...
#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

const int N=510,M=60010;

int n,m;

struct BIT {
	int lowbit(int x) {
		return x&-x;
	}
	int n;
	int tr[N][N];
	void update(int x,int y,int k) {
		for(int i=x;i<=n;i+=lowbit(i))
			for(int j=y;j<=n;j+=lowbit(j))
				tr[i][j]+=k;
	}
	int query(int x,int y) {
		int res=0;
		for(int i=x;i;i-=lowbit(i))
			for(int j=y;j;j-=lowbit(j))
				res+=tr[i][j];
		return res;
	}
	int submatr(int x1, int y1, int x2, int y2) {
		return query(x2,y2)-query(x1-1,y2)-query(x2,y1-1)+query(x1-1,y1-1);
	}
}T;

struct Num {
	int x,y;
	int val;
	friend bool operator < (Num A,Num B) {
		return A.val<B.val;
	}
}mat[N*N];
int cm;

struct Query {
	int x1,y1,x2,y2,k;
}q[M];

int id[M];
int t1[M],t2[M];

int ans[M],cur[M];

int count_black(Query Q) {
	return T.submatr(Q.x1,Q.y1,Q.x2,Q.y2);
}

#define mid ((lft+rgh)/2)
void solve(int lft,int rgh,int ql,int qr) {
	if(ql>qr) return;
	if(lft==rgh) {
		for(int i=ql;i<=qr;i++) ans[id[i]]=mat[lft].val;
		return;
	}
	for(int i=lft;i<=mid;i++) T.update(mat[i].x,mat[i].y,1);
	int c1=0,c2=0;
	for(int i=ql;i<=qr;i++) {
		int nw=id[i];
		int sum=cur[nw]+count_black(q[nw]);
		if(sum>=q[nw].k) t1[++c1]=nw;
		else t2[++c2]=nw,cur[nw]=sum;
	}
	int qc=ql-1;
	for(int i=1;i<=c1;i++) id[++qc]=t1[i];
	for(int i=1;i<=c2;i++) id[++qc]=t2[i];
	for(int i=lft;i<=mid;i++) T.update(mat[i].x,mat[i].y,-1);
	solve(lft,mid,ql,ql+c1-1),solve(mid+1,rgh,ql+c1,qr);
}

int main() {
	scanf("%d%d",&n,&m);
	T.n=n;
	for(int i=1;i<=n;i++) {
		for(int j=1;j<=n;j++) {
			cm++;
			mat[cm].x=i,mat[cm].y=j;
			scanf("%d",&mat[cm].val);
		}
	}
	sort(mat+1,mat+cm+1);
	for(int i=1;i<=m;i++) {
		scanf("%d%d%d%d%d",&q[i].x1,&q[i].y1,&q[i].x2,&q[i].y2,&q[i].k);
		id[i]=i; 
	}
	solve(1,cm,1,m);
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	
	return 0;
} 

混合果汁

整体二分 + 线段树上二分

对于一个单独的询问,显然可以二分
对于所有美味度大于当前的二分值的果汁,贪心地选择便宜的,一直买到小朋友满意为止,然后看钱够不够

这个过程可以用线段树上的二分实现
建一棵权值线段树,下标是果汁的价格,每个节点维护价格在当前区间的所有果汁的总量和总价钱
每到一个节点,看左儿子的总量够不够,够就递归左边,不够就递归右边

多次询问可以考虑整体二分

每次调整权值线段树内维护的是从 1 到 middle 即可

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long ll;

const int MX=100000,N=MX+10;
const int Inf=2e9;

int n,m;

struct Segtree {
	#define ls (u<<1)
	#define rs (u<<1|1)
	struct Node {
		ll sum,num;
	}tr[N*4];
	void pushup(int u) {
		tr[u].num=tr[ls].num+tr[rs].num;
		tr[u].sum=tr[ls].sum+tr[rs].sum;
	}
	void update(int u,int lft,int rgh,int p,int k) {
		if(lft==rgh) {
			tr[u].num+=k;
			tr[u].sum=1ll*lft*tr[u].num;
			return;
		}
		int mid=lft+rgh>>1;
		if(p<=mid) update(ls,lft,mid,p,k);
		else update(rs,mid+1,rgh,p,k);
		pushup(u); 
	}
	ll query(int u,int lft,int rgh,ll k) {
		if(!k) return 0;
		if(lft==rgh) return k*lft;
		int mid=lft+rgh>>1;
		if(tr[ls].num>=k) return query(ls,lft,mid,k);
		return tr[ls].sum+query(rs,mid+1,rgh,k-tr[ls].num);
	}
}T;

struct Juice {
	int val,pri,lim;
	friend bool operator < (Juice A,Juice B) {
		return A.val>B.val;
	}
}w[N];

struct Child {
	int id;
	ll csh,ltr;
}q[N],t1[N],t2[N];

int ans[N];
int now;

void solve(int lft,int rgh,int ql,int qr) {
	if(ql>qr) return;
	if(lft==rgh) {
		for(int i=ql;i<=qr;i++) ans[q[i].id]=w[lft].val;
		return;
	}
	int mid=lft+rgh>>1;
	while(now<mid) now++,T.update(1,1,MX,w[now].pri,w[now].lim);
	while(now>mid) T.update(1,1,MX,w[now].pri,-w[now].lim),now--;
	int c1=0,c2=0;
	for(int i=ql;i<=qr;i++) {
		if(T.tr[1].num<q[i].ltr) t2[++c2]=q[i];
		else if(T.query(1,1,MX,q[i].ltr)<=q[i].csh) t1[++c1]=q[i];
		else t2[++c2]=q[i];
	}
	int qc=ql-1;
	for(int i=1;i<=c1;i++) q[++qc]=t1[i];
	for(int i=1;i<=c2;i++) q[++qc]=t2[i];
	solve(lft,mid,ql,ql+c1-1),solve(mid+1,rgh,ql+c1,qr);
}

int main() {
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++) scanf("%d%d%d",&w[i].val,&w[i].pri,&w[i].lim);
	for(int i=1;i<=m;i++) scanf("%lld%lld",&q[i].csh,&q[i].ltr),q[i].id=i;
	w[++n]={-1,0,Inf};
	sort(w+1,w+n+1);
	solve(1,n,1,m);
	for(int i=1;i<=m;i++) printf("%d\n",ans[i]);
	
	return 0;
}

整体二分总结

整体二分的模板:

void solve(int lft,int rgh,int ql,int qr) {
	if(ql>qr) return; //当前区间没有询问 
	if(lft==rgh) { //找到答案 
		for(int i=ql;i<=qr;i++) //记录答案 
		return;
	}
	int mid=lft+rgh>>1;
	//调整数据结构内维护的值为从 1 到 mid 
    //有两种方法
    //   1.调整,多了就删,少了就加
    //   2.每次暴力把从 lft 到 mid 暴力加进来,后面清除
    //     并且对于分到右边的询问,记下当前的值(因为后面只会考虑右半边的)
	int c1=0,c2=0; //分成两部分 
	for(int i=ql;i<=qr;i++) {
		//对于每一个询问进行 check
		//并分成两部分 
	}
	//把临时数组复制到查询的数组
	//改变顺序,分成两拨 
	solve(lft,mid,ql,ql+c1-1),solve(mid+1,rgh,ql+c1,qr);//递归进行下一层二分 
}

骑士

\(n\) 个点 \(n\) 条边 ,每个点最多有一条出边
因此这是一个 基环树 森林(可能不连通)
每一个连通块有且仅有一个环
因此我们可以找到换上的两个点
割掉这两个点之间的边
于是这道题就变成了树形DP入门题 没有上司的舞会
还有一件事
这两个点也不能同时选
我们以这两个点为根分别跑一遍树形DP
每次都强制根节点不能选,两遍的答案取 max 就行

最后的答案就是每个连通块的答案之和

#include<iostream>
#include<cstring>
#include<algorithm>

using namespace std;

typedef long long ll;

const int N=1e6+10,M=2*N;

int n;
bool vis[N];
ll w[N],f[N][2];
int x,y,delE; 

int hd[N],edg[M],nxt[M],idx;

void adde(int u,int v) {
	edg[idx]=v,nxt[idx]=hd[u],hd[u]=idx++;
}

void circle(int u,int p) {
	vis[u]=true;
	for(int i=hd[u];~i;i=nxt[i]) {
		int v=edg[i];
		if(v==p) continue;
		if(vis[v]) {
			x=u,y=v;
			delE=i;
			continue;
		}
		circle(v,u);
	}
}

void DP(int u,int p) {
	f[u][1]=w[u],f[u][0]=0;
	for(int i=hd[u];~i;i=nxt[i]) {
		int v=edg[i];
		if(v==p) continue;
		if(i==delE||(i^1)==delE) continue;
		DP(v,u);
		f[u][1]+=f[v][0];
		f[u][0]+=max(f[v][0],f[v][1]);
	}
}

int main() {
	memset(hd,-1,sizeof(hd));
	scanf("%d",&n);
	for(int u=1;u<=n;u++) {
		int v;
		scanf("%lld%d",&w[u],&v);
		adde(u,v),adde(v,u);
	}
	ll ans=0;
	for(int i=1;i<=n;i++) {
		if(vis[i]) continue;
		circle(i,-1);
		DP(x,-1);
		ll tmp=f[x][0];
		DP(y,-1);
		tmp=max(tmp,f[y][0]);
		ans+=tmp;
	}
	printf("%lld\n",ans);
	
	return 0;
}

晚上和机房同学分享题目

给大家讲莫队讲崩了,讲树形DP也讲崩了hhh
自己想的时候思路很清晰,一到台上就不知道要说啥了,语言也组织不好
还得练习讲题的能力啊 =_=

bpc 分享的莫队题,他解释的题意我理解错了,半天搞不懂,重读了一遍题就会了
时间不够了,明天再写吧

udpate: finished

posted @ 2024-03-14 18:07  OrangeStar*  阅读(13)  评论(1编辑  收藏  举报