D.Two-Colored Dominoes
题目描述:
有一个\(n\times m\)棋盘,被分成若干小格。棋盘上还有一些多米诺骨牌。每张骨牌覆盖相邻的两个小格(即共用一条边的两个小格),没有两张骨牌重叠。
皮特认为这块棋盘太无聊了,需要涂上颜色。他要把多米诺骨牌的格子涂成黑色和白色。如果以下所有条件都成立,他就认为这幅画很漂亮:
- 每张多米诺骨牌的其中一个格子涂成白色,另一个格子涂成黑色;
- 对于每一行,该行的黑色格子数等于该行的白色格子数;
- 对于每一列,该列的黑色单元格数等于该列的白色单元格数。
请注意,没有被多米诺骨牌覆盖的单元格根本不会被涂上任何颜色,它们既不被算作黑色,也不被算作白色。
帮助皮特画出一幅美丽的画,或者告诉他这是不可能的。
\(2 \le n,m \le 500\)
题目分析:
考虑对于行的限制会产生影响的只有竖着放置的骨牌,因为横着放置的骨牌的贡献一定是一个黑一个白也就是不会产生影响。
看到这种跟两行相关的不难想到:对于每一个骨牌就给覆盖的两行连一条边。
这样连出来就是一条有重边的链,可以发现的是链端点的这一行只有与下一行连边这一种连边,所以对于端点来说可以确定这些连边里面肯定是一半的黑一半的白。
所以可以记录每一行现在有了多少个黑格子多少个白格子,然后从链的端点开始推就可以了,可以发现我们每一次推到某一行时都只有一种连边方式需要我们去决策,直接调整到相等即可。
对于列同理。
代码:
点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N = 1000;
char s[N][N],t[N][N];
int cnt[N],w[N],b[N];
int main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%d",&T);
while(T--){
int n,m;scanf("%d%d",&n,&m);
for(int i=1; i<=n; i++) scanf("%s",s[i]+1);
bool flag = false;
//对于行处理
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
t[i][j] = s[i][j];
if(s[i][j] == 'U') cnt[i]++;
}
}
for(int i=1; i<=n; i++){
if((cnt[i-1] + cnt[i]) & 1) flag = true;
int tmp = (cnt[i-1] + cnt[i]) / 2;
if(w[i] > tmp || b[i] > tmp) flag = true;
for(int j=1; j<=m; j++){
if(s[i][j] == 'U'){
if(w[i] < tmp) t[i][j] = 'W',t[i+1][j] = 'B',w[i]++,b[i+1]++;
else t[i][j] = 'B',t[i+1][j] = 'W',b[i]++,w[i+1]++;
}
}
}
for(int i=1; i<=n; i++){
if(w[i] != b[i]){
flag = true;
}
}
for(int i=1; i<=n; i++) cnt[i] = 0,w[i] = 0,b[i] = 0;
//对于列处理
for(int i=1; i<=m; i++){
for(int j=1; j<=n; j++){
if(s[j][i] == 'L') cnt[i]++;
}
}
for(int i=1; i<=m; i++){
if((cnt[i] + cnt[i-1]) & 1) flag = true;
int tmp = (cnt[i] + cnt[i-1]) / 2;
if(w[i] > tmp || b[i] > tmp) flag = true;
for(int j=1; j<=n; j++){
if(s[j][i] == 'L'){
if(w[i] < tmp) t[j][i] = 'W',t[j][i+1] = 'B',w[i]++,b[i+1]++;
else t[j][i] = 'B',t[j][i+1] = 'W',b[i]++,w[i+1]++;
}
}
}
for(int i=1; i<=n; i++){
if(w[i] != b[i]){
flag = true;
}
}
for(int i=1; i<=m; i++) cnt[i] = 0,w[i] = 0,b[i] = 0;
if(flag) printf("-1\n");
else{
for(int i=1; i<=n; i++){
for(int j=1; j<=m; j++){
printf("%c",t[i][j]);
}
printf("\n");
}
}
}
return 0;
}
E.Speedrun
题目描述:
您正在玩一款视频游戏。游戏中有 \(n\) 个任务需要完成。但是,第 \(j\) 个任务只能在游戏日的第 \(h_j\) 个小时开始时完成。每一个游戏日为 \(k\) 小时。第一天结束后,新的一天开始,依此类推。
此外,任务之间存在依赖关系,即对于某些任务对 \((a_i, b_i)\) 来说,\(b_i\) 个任务只能在 \(a_i\) 个任务之后完成。保证不存在循环依赖,否则游戏将是不可战胜的,也不会有人玩。
您有足够的技能在可以忽略不计的时间内完成任意数量的任务(也就是说,您可以在同一小时开始时完成任意数量的任务,即使它们之间存在依赖关系)。您希望尽快完成所有任务。为此,您可以按照任何有效顺序完成任务。完成时间等于完成最后一个任务的时间与按此顺序完成第一个任务的时间之差。
找出完成游戏所需的最少时间。
\(n,m \le 2·10^5\)
题目分析:
这种依赖关系显然可以想到连边,也就是对于限制 \((a_i,b_i)\) 则连边 \((a_i,b_i)\),则连出来的一定是一个 DAG。
可以发现的是我们最关键的一个任务就是第一个完成任务,因为当我们确定了第一个完成的任务之后的任务只需要按照贪心的想法能完成则完成即可。
所以一个想法就是枚举第一个完成的任务,而对于其他 DAG 其对应的最优操作方式肯定是不变的,所以需要预处理出 \(f_i\) 表示以 \(i\) 为第一个完成的任务,则仅考虑 \(i\) 能走到的点其最少需要的时间是多少。
转移如下:
但是我们发现这样的统计答案依旧是不好做,因为 DAG 不连通。
假设我们选择的第一个完成的任务的时间为 \(s\) 则可以理解为建立一个虚拟点 \(x\) 从 \(x\) 向其他所有可以被选择为第一个任务的点连边,且 \(h_x = s\)。
这样的操作之后我们的 DAG 就能变成一个联通的 DAG,这样直接求出 \(f_x\) 就是答案。
这个时候 \(f_x\) 的转移就相当于一个前缀/后缀 \(\max\) 查询,就直接线段树维护就可以了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 4e5+5;
const int INF = 1e18;
struct edge{
int nxt,to;
edge(int _nxt,int _to){
nxt = _nxt,to = _to;
}
edge(){}
}e[2 * N];
int cnt,head[N],h[N],a[N],f[N],mx[4 * N],deg[N];
bool flag[N];
void add_edge(int from,int to){
e[++cnt] = edge(head[from],to);
head[from] = cnt;
}
int query(int now,int now_l,int now_r,int l,int r){
if(l > r) return -INF;
if(l <= now_l && now_r <= r) return mx[now];
int mid = (now_l + now_r)>>1,ans = -INF;
if(l <= mid) ans = max(ans,query(now<<1,now_l,mid,l,r));
if(r > mid) ans = max(ans,query(now<<1|1,mid+1,now_r,l,r));
return ans;
}
void modify(int now,int now_l,int now_r,int pos,int val){
if(now_l == now_r){
mx[now] = max(mx[now],val);
return;
}
int mid = (now_l + now_r)>>1;
if(pos <= mid) modify(now<<1,now_l,mid,pos,val);
if(pos > mid) modify(now<<1|1,mid+1,now_r,pos,val);
mx[now] = max(mx[now<<1],mx[now<<1|1]);
}
void build(int now,int now_l,int now_r){
mx[now] = -INF;
if(now_l == now_r) return;
int mid = (now_l + now_r)>>1;
build(now<<1,now_l,mid);build(now<<1|1,mid+1,now_r);
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
int n,m,k;scanf("%lld%lld%lld",&n,&m,&k);
for(int i=1; i<=n; i++) scanf("%lld",&h[i]);
for(int i=1; i<=m; i++){
int a,b;scanf("%lld%lld",&a,&b); //建反图跑拓扑序
add_edge(b,a);deg[a]++;flag[b] = true;
}
queue<int> q;
for(int i=1; i<=n; i++){
if(!deg[i]) q.push(i);
}
while(!q.empty()){
int now = q.front();q.pop();
for(int i=head[now]; i; i=e[i].nxt){
int to = e[i].to;
int cost = 0;
if(h[to] > h[now]) cost = h[now] + k - h[to];
else cost = h[now] - h[to];
f[to] = max(f[to],f[now] + cost);deg[to]--;
if(!deg[to]) q.push(to);
}
}
for(int i=1; i<=n; i++) a[i] = h[i];
int tot = n;
sort(a+1,a+n+1);tot = unique(a+1,a+tot+1) - a - 1;
for(int i=1; i<=n; i++) h[i] = lower_bound(a+1,a+tot+1,h[i]) - a;
build(1,1,n);
for(int i=1; i<=n; i++){ //寻找可以在第一步选择的点
if(!flag[i]){
modify(1,1,tot,h[i],f[i] + a[h[i]]);
}
}
int ans = INF;
for(int i=1; i<=n; i++){ //枚举第一个位置
if(!flag[i]){
int ans1 = query(1,1,tot,1,h[i]-1) + k - a[h[i]];
int ans2 = query(1,1,tot,h[i],tot) - a[h[i]];
ans = min(ans,max(ans1,ans2));
}
}
printf("%lld\n",ans);
cnt = 0;
for(int i=1; i<=n; i++) head[i] = 0,f[i] = 0,flag[i] = false,deg[i] = 0;
}
return 0;
}
F.Divide, XOR, and Conquer
题目描述:
给你一个由 \(n\) 个整数 \(a_1, a_2, \ldots, a_n\) 组成的数组。
通过一次操作,您可以将数组分成两部分:非空前缀和非空后缀。每个部分的值都是其中所有元素的 bitwise XOR。接下来,丢弃值较小的部分。如果两部分的值相等,可以选择丢弃哪一部分。用剩下的部分替换数组。
上述操作一直进行到数组的长度变为 \(1\)为止。对于每一个 \(i\)(\(1 \le i \le n\)),判断是否有可能达到只剩下第 \(i\)个元素(相对于原始编号)时的状态。
形式上,有两个数字 \(l\) 和 \(r\),最初分别是 \(l = 1\) 和 \(r = n\)。数组的当前状态为 \([a_l, a_{l+1}, \ldots, a_r]\)。
只要有 \(l < r\),就可以执行下面的操作:
- 从集合\(\{l, l + 1, \ldots, r - 1\}\)中任意选择一个\(k\)。表示 \(x = a_l \oplus a_{l + 1} \oplus \ldots \oplus a_k\)和 \(y = a_{k + 1} \oplus a_{k + 2} \oplus \ldots \oplus a_{r}\),其中 \(\oplus\)表示位向 XOR 运算。
- 若\(x < y\),设\(l = k + 1\)。
- 若\(x > y\),则设置\(r = k\)。
- 若\(x = y\),要么设\(l = k + 1\),要么设\(r = k\)。
对于每个 \(i\)(\(1 \le i \le n\)),确定是否有可能实现 \(l = r = i\)。
\(1 \le n \le 10^4\)
题目分析:
设 \(s = a_l \oplus a_{l+1} \oplus \cdots \oplus a_r,x = a_l \oplus a_{l+1} \oplus \cdots \oplus a_k,y = a_{k+1} \oplus a_{k+2} \oplus \cdots \oplus a_r\)。
若 \(s = 0\),则任意一个 \(k\) 都是合法的,因为 \(x \oplus y = 0 \to x = y\)。
若 \(s \not= 0\),则一个合法的操作必然满足 \(s,x\) 二进制下最高的 \(1\) 的位置相同,若不相同则显然 \(y > x\),而相同则必然存在 \(x > y\)。
所以可以考虑统计 \(SL[i]\) 表示左端点为 \(i\) 所有合法区间的 \(s\) 的最高的 \(1\) 的位置的集合,\(SR[i]\) 表示右端点为 \(i\) 的对应的集合。
我们如果从长到短依次考虑每一个区间,则当我们考虑到区间 \([l,r]\) 时,当 \(s\) 的二进制下为 \(1\) 的位与 \(SL[l]\) 或 \(SR[r]\) 有交时则意味着 \([l,r]\) 可以通过某些操作得到。
然后就做完了。
代码:
点击查看代码
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1e4+5;
const int INF = (1ll<<61)-1;
int a[N],SL[N],SR[N];
bool dp[N][N];
int get(int x){
if(!x) return INF; //所有小的都可以
return 1ll<<(63-__builtin_clzll(x));
}
signed main(){
// freopen("in.txt","r",stdin);
// freopen("out.txt","w",stdout);
int T;scanf("%lld",&T);
while(T--){
int n;scanf("%lld",&n);
for(int i=1; i<=n; i++) scanf("%lld",&a[i]),a[i] = a[i] ^ a[i-1];
dp[1][n] = 1;
for(int len=n; len>=1; len--){
for(int l=1; l + len - 1 <= n; l++){
int r = l + len - 1;
int x = a[r] ^ a[l-1];
if(dp[l][r] || (x & SL[l]) || (x & SR[r]) || (SL[l] >= INF) || (SR[r] >= INF)){
dp[l][r] = 1;
SL[l] |= get(x);SR[r] |= get(x);
}
}
}
for(int i=1; i<=n; i++) printf("%d",dp[i][i]);
printf("\n");
for(int i=1; i<=n; i++){
SL[i] = SR[i] = 0;
for(int j=i; j<=n; j++){
dp[i][j] = 0;
}
}
}
return 0;
}