牛客 周赛89 20250415

牛客 周赛89 20250415

https://ac.nowcoder.com/acm/contest/107000

A:

题目大意:

void solve(){
	int n;
	cin>>n;
	cout<<(n+1)/2<<endl;
}

签到

B:

题目大意:给定 \(n\) 盏灯的状态,需要使得相邻的灯都不同时亮,求操作次数

void solve(){
	int n;
	cin>>n;
	vector<char> a(n+10);
	for (int i=1;i<=n;i++) cin>>a[i];
	int cnt=0;
	for (int i=2;i<=n;i++){
		if (a[i-1]=='1'&&a[i]=='1'){
			a[i]='0';
			cnt++;
		}
	}
	cout<<cnt;
}

贪心即可,遇到相邻的 \(1\) 就关闭后一盏灯

C:

题目大意:有 \(n\) 盏灯最开始都是开的,每次可以改变连续的 \(k\) 盏灯的状态,求能组合出的所有状态数

void solve(){
    int n,k;
    cin>>n>>k;
    LL ans=1;
    for (int i=1;i<=(n-k)+1;i++)
        ans=(ans*2)%mod;
    cout<<ans;
}

考虑这样的组合

对于每个长度为 \(k\) 的区间,对 \([1,n]\) 进行修改,那么可以对 \(n-k+1\) 个不同的区间修改,例如

\([1,k],[2,k+1],[3,k+2],\cdots,[n-k+1,n]\)

对于每个区间,有两种操作方案,因为对同一盏灯操作两次相当于回到初始状态

现在分析对不同区间的操作产生的结果都是独立的,形象化的,即 \([1,k],[2,k+1]\)\([1,k],[3,k+2]\) 操作这两个区间组合产生的状态都是不同的

\[[1,k],[2,k+1]\to\{1\},[2,k],\{k+1\} \\ [1,k],[3,k+2]\to\{1,2\},[3,k],\{k+1,k+2\} \]

可以看到,这两个组合重合的部分为 \(\{1\},[3,k+1],\{k+1\}\) 其余的部分导致这两个组合对答案的贡献都是独立的,所以不存在交集

那么总的状态数可以利用乘法原理计算,即

\[ans=2^{n-k+1} \]

D:

题目大意:与 C 背景相同,现在每次操作可以改变任意的 \(k\) 盏灯

void solve(){
    int n,k;
    cin>>n>>k;
    LL ans=1;
    if (k==n){
        cout<<2;
        return;
    }
    for (int i=1;i<=n-1;i++)
        ans=(ans*2)%mod;
    if (k%2) cout<<(ans*2)%mod;
    else cout<<ans;
}

先说结论,当 \(k\) 为偶数时答案是 \(2^{n-1}\)\(k\) 为奇数时答案为 \(2^n\)

可以将所有灯的状态表示为一个长度为 \(n\) 的在模 \(2\) 空间 \(F_2^n\) 下的向量,所有可达状态被表示为初始状态加上操作向量的线性组合,每次操作相当于选择 \(F_2^n\) 的一个大小为 \(k\) 的子集,将这些位置上的状态取反

\[ans={\rm{sumof}}\{(1,1,\cdots,1)+{\rm{span}}(V)\} \]

所以可达状态的数量等于操作向量生成的线性子空间的大小 \(2^D\) ,现在考虑 \(k\)\(D\) 的影响

\(k\) 为奇数时,可以构造出仅使一盏灯翻转的操作向量线性组合,简单证明有:

对于任意的 \(k\) 都能构造出仅使两盏灯翻转的操作向量组合,例如操作 \([1,k],[2,k+1]\) ,翻转的只有 \({1,k+1}\)

又因为任意一个奇数 \(od\) 都能被表示为 \(2n+1\) ,那么在 \(k\) 为奇数时,翻转 \(k\) 盏灯后再通过某些操作向量线性组合可以将 \(k-1\) 盏灯翻回去,仅剩余 \(1\) 盏灯翻转,证毕。

所以这种情况下的状态总数为 \(2^n\)\(D=n\)

\(k\) 为偶数时,固定一盏灯会被翻转,那么剩下的 \(k-1\) 盏灯在 \(n-1\) 中选取,所以这盏灯在所有的状态中被翻转次数为 \(n-1\choose k-1\)

又因为 \(k\) 为偶数时 \(n-1\choose k-1\) 也为偶数,那么在 \(F_2^n\) 下的操作向量线性组合总和为 \(0\) ,至多有 \(n-1\) 个向量线性无关, \(D\le n-1\)

或者从组合数学的角度来看

\(k\) 为奇数时,可以任意选择一盏灯翻转,那么总状态数为:

\[\sum_{i=0}^n {n\choose i}=2^n \]

\(k\) 为偶数时,任意选取两盏灯进行翻转,总状态数表示为:

\[{n\choose 0}+{n\choose 2}+\cdots+{n\choose 2k},(2k\le n) \]

帕斯卡法则 \({n\choose k}={n-1\choose k-1}+{n-1\choose k}\) 得到

\[{n\choose 0}+{n\choose 2}+\cdots+{n\choose 2k}=2^{n-1} \]

E:

题目大意:

char g[110][110];
int dx[]={1,0,-1,0};
int dy[]={0,1,0,-1};
int n,m;
vector<pair<int,int>> path;
bool vis[110][110];
vector<pair<int,int>> ans;
int sumop;
 
bool dfs(int sx,int sy,int ex,int ey){
     
    if (sx==ex&&sy==ey){
        return 1;
    }
     
    for (int i=0;i<4;i++){
        int tx=sx+dx[i],ty=sy+dy[i];
        if (tx<1||ty<1||tx>n||ty>m) continue;
        if (abs(ex-sx)<abs(ex-tx)||abs(ey-sy)<abs(ey-ty)) continue;
        if (vis[tx][ty]) continue;
        vis[tx][ty]=1;
        path.push_back({tx,ty});
        if (dfs(tx,ty,ex,ey)) return 1;
        path.pop_back();
         
    }
    return 0;
}
 
void solve(){
     
    cin>>n>>m;
    int sum0=0; 
    for (int i=1;i<=n;i++){
        for (int j=1;j<=m;j++){
            cin>>g[i][j];
            if (g[i][j]=='0') sum0++;
        }
    }
    if (sum0%2){
        cout<<-1;
        return;
    }
    int cnt=0;
    pair<int,int> p[3];
    for (int i=1;i<=n;i++){
        for(int j=1;j<=m;j++){
            if (g[i][j]=='0'){
                cnt++;
                p[cnt]={i,j};
                if (cnt==2){
                    memset(vis,0,sizeof vis);
                    g[p[1].first][p[1].second]='1';
                    g[p[2].first][p[2].second]='1';
                    cnt=0;
                    if (dfs(p[1].first,p[1].second,p[2].first,p[2].second)){
                        sumop+=path.size();
                         
                        ans.push_back({p[1].first,p[1].second});
                         
                        for (int i=0;i<path.size()-1;i++){
                            ans.push_back({path[i].first,path[i].second});
                            ans.push_back({path[i].first,path[i].second});
                        }
                         
                        ans.push_back({p[2].first,p[2].second});
                    }
                    path.clear();
                     
                }
            }
        }
    }
    cout<<sumop<<endl;
    for (int i=0;i<ans.size();i++){
        if (i%2==0){
            cout<<ans[i].first<<' '<<ans[i].second<<' ';
        }
        else
            cout<<ans[i].first<<' '<<ans[i].second<<endl;
    }
}

构造题写的有点屎山,但是时间复杂度还行

每次可以操作两盏灯,显然如果关闭的灯为奇数不存在答案

原来的想法是每次找两个关闭的灯,DFS计算路径,加上剪枝优化

实际上可以逐行贪心地构造,如果枚举到 0 那么就令他和后一个元素同时改变

两层 for

for (int i=1;i<=n;i++)
		for (int j=1;j<m;j++)

这样操作下来,最后仍然关闭的灯就全部集中在最后一列

for (int i=1;i<n;i++){
	if (g[i][m]=='0'){
		g[i][m]='1';
		g[i+1][m]=g[i+1][m]=='0'?'1':'0';
		path.push_back(i);
		path.push_back(m);
		path.push_back(i+1);
		path.push_back(m);
	}
}

同样的操作使得最后一列自上而下进行操作,最后灯一定会全部关闭

char g[110][110];

void solve(){
	int n,m;
	cin>>n>>m;
	int cnt=0;
	for (int i=1;i<=n;i++){
		for (int j=1;j<=m;j++){
			cin>>g[i][j];
			if (g[i][j]=='0') cnt++;
		}
	}	
	if (cnt%2){
		cout<<-1;
		return;
	}
	vector<int> path;
	for (int i=1;i<=n;i++){
		for (int j=1;j<m;j++){
			if (g[i][j]=='0'){
				g[i][j]='1';
				g[i][j+1]=g[i][j+1]=='0'?'1':'0';
				path.push_back(i);
				path.push_back(j);
				path.push_back(i);
				path.push_back(j+1);
			}
		}
	}
	for (int i=1;i<n;i++){
		if (g[i][m]=='0'){
			g[i][m]='1';
			g[i+1][m]=g[i+1][m]=='0'?'1':'0';
			path.push_back(i);
			path.push_back(m);
			path.push_back(i+1);
			path.push_back(m);
		}
	}
	cout<<path.size()/4<<endl;
	for (int i=0;i<path.size();i+=4){
		cout<<path[i]<<' '<<path[i+1]<<' '<<path[i+2]<<' '<<path[i+3]<<endl;
	}
}

F:
题目大意:给定一棵树,树上的节点一开始都为亮,每次可以改变相邻两盏灯的状态,在保证最后没有两盏灯相邻且同时开启的状态的下,求最少的操作次数

int n;
vector<int> e[100010];
LL dp[100010][3];

void dfs(int x,int p){
	dp[x][0]=1;
	LL ma=1e9;
	for (auto v:e[x]){
		if (v==p) continue;
		dfs(v,x);
		dp[x][0]+=min(dp[v][1],dp[v][2]);
		dp[x][1]+=min(dp[v][1],dp[v][2]);
		dp[x][2]+=dp[v][1];
		ma=min(ma,dp[v][0]-min(dp[v][1],dp[v][2]));
	}
	if (e[x].size()>1||x==1) dp[x][1]+=ma;
	if (e[x].size()==1&&x!=1) dp[x][1]=1e9;
	
}

void solve(){
	cin>>n;
	for (int i=1;i<n;i++){
		int u,v;
		cin>>u>>v;
		e[u].push_back(v);
		e[v].push_back(u);
	}	
	dfs(1,0);
	cout<<min(dp[1][1],dp[1][2]);
}

树形DP,没怎么练习过

存在这样一个结论,一个节点最多只会被操作一次,即只能从亮变暗不能从暗变亮

定义 \(dp_{i,0}\) 表示当 \(i\) 节点与他的父亲一起变暗的最少操作次数

\(dp_{i,1}\) 表示当 \(i\) 节点没有被操作时,他的一个儿子被操作的最少操作数

\(dp_{i,2}\) 表示当 \(i\) 节点没有被操作时,他的所有儿子都被操作的最少操作数

我们需要同时操作两盏灯,又因为一个节点存在多个儿子,不方便定位儿子的位置,所以就定位节点的唯一的父亲

考虑状态的转移,初始化时 \(dp_{i,0}=1\)

当一个父节点为暗时,他的儿子可以被约束为任意状态(子节点不影响父节点)

当父节点为亮时,为了满足题意,要么存在一个子节点被操作(父子同时变暗),或者所有的子节点都变暗(父节点可亮可暗)

  • \(dp_{i,0}=\sum{\rm{min}}(dp_{j,1},dp_{j,2})\) ,即当前父节点如果为暗,那么递推的操作数为他的子节点上传的操作数总和(此时不考虑 \(dp_{j,0}\) 的影响,是因为前面得出的一个节点最多只会被操作一次的结论,所以父节点被操作时,不会从子节点被操作的状态转移

  • \(dp_{i,1}=\sum{\rm{min}}(dp_{j,1},dp_{j,2})\) ,父节点为亮,选择第一个选项:他的一个子节点被操作

    同样的根据结论,我们只能操作 \(j\) 节点的儿子来对 \(j\) 进行操作。选择某个子节点操作时,利用 \(ma\) 记录选 \(i\) 节点的哪个儿子能使得操作数最小,ma=min(ma,dp[v][0]-min(dp[v][1],dp[v][2])); 表示将子节点 \(j\) 的状态从 \(\sum{\rm{min}}(dp_{j,1},dp_{j,2})\) 切换到 \(dp_{j,0}\) 所需的额外操作数

  • \(dp_{i,2}=\sum dp_{j,1}\) ,父节点为亮,选择第二个选项:他的所有子节点都被操作(子节点都为暗)

    \(dp_{i,0}\) 类似,不考虑 \(dp_{j,0}\) 的影响,所以只能从 \(dp_{j,1}\) 转移状态

遍历完当前父节点的所有儿子后,我们应当更新 \(dp_{i,1}+=ma\) 将操作 \(i\) 节点的某一个儿子的操作数最小化

如果当前节点为叶子节点,叶子节点不存在儿子,所以 \(dp_{i,0}=INF\) 表示不可选

所以最终答案为 \({\rm{min}}(dp_{1,1},dp_{1,2})\)

posted @ 2025-04-20 16:23  才瓯  阅读(15)  评论(0)    收藏  举报