周赛地址:https://leetcode-cn.com/contest/weekly-contest-231/
第一题:检查二进制字符串字段
由于没有前导0,如果字符串里有1,那么字符串第一个一定是1,遍历的时候,先找0再找1,如果有这样的情况,就返回false。
换言之,就是在字符串中找“01”子串。有“01”子串的返回false,否则返回true。
自己写的有点麻烦了,贴一个简洁的代码。
class Solution {
public boolean checkOnesSegment(String s) {
return !s.contains("01");
}
}
第二题:构成特定和需要添加的最少元素
需要注意数据范围。
1
<
=
n
u
m
s
.
l
e
n
g
t
h
<
=
1
0
5
1 <= nums.length <= 10^{5}
1<=nums.length<=105
1
<
=
l
i
m
i
t
<
=
1
0
6
1 <= limit <= 10^{6}
1<=limit<=106
−
l
i
m
i
t
<
=
n
u
m
s
[
i
]
<
=
l
i
m
i
t
-limit <= nums[i] <= limit
−limit<=nums[i]<=limit
−
1
0
9
<
=
g
o
a
l
<
=
1
0
9
-10^{9} <= goal <= 10^{9}
−109<=goal<=109
在求和的时候,考虑极端情况,
s
u
m
=
1
0
5
×
1
0
6
=
1
0
11
sum=10^{5}\times10^{6}=10^{11}
sum=105×106=1011,已经超过int了,所以一些中间结果要用long保存。
这里说向数组中添加的最少数量,每次添加的值,范围要在[-limit, limit]范围内,很容易想到,每次添加就选择limit即可,最后一次根据实际情况选择。
class Solution {
public int minElements(int[] nums, int limit, int goal) {
int length = nums.length;
long sum = 0;
for (int i : nums) {
sum += i;
}
long diff = Math.abs(goal - sum);
long mod = diff % limit;
long div = diff / limit;
if (mod != 0) {
div++;
}
return (int) div;
}
}
看题解的时候,看到一个写法,记录下来。
if (diff == 0) {
return 0;
}
return (diff - 1) / limit + 1;
这里的(diff - 1) / limit + 1相当于Math.ceil(diff / limit)了。
第三题:从第一个节点出发到最后一个节点的受限路径数
根据题目描述和示例,先说下什么叫受限路径。
观察示例1,首先求得1~n各个结点到n结点的最短路径,当
d
i
s
t
a
n
c
e
[
i
]
>
d
i
s
t
a
n
c
e
[
i
的
邻
接
点
]
distance[i]>distance[i的邻接点]
distance[i]>distance[i的邻接点],那么(i, i的邻接点)就是受限路径。
在无向图中,求解1~n各个结点到n结点的最短路径,等于求解从n出发到1~n各个顶点的最短路径,因为无向图中
w
(
i
,
j
)
=
w
(
j
,
i
)
w_{(i, j)}=w_{(j, i)}
w(i,j)=w(j,i),所以这里采用单源求解最短路算法Dijkstra即可。
再说求解受限路径的条数,结点1到结点n的受限路径条数=sum(结点1的邻接点到n受限路径条数),避免重复计算,使用记忆化搜索。
class Solution {
HashMap<Integer, ArrayList<Edge>> graph;// 邻接表存储图
int[] distance;// Dijkstra用到的distance数组
boolean[] visit;// Dijkstra用到的visit数组
PriorityQueue<Edge> priorityQueue;// Dijkstra使用堆优化
long[] memory;// 用于记忆化搜索的数组
int MOD = 1000000007;// MOD = 1e9 + 7
public int countRestrictedPaths(int n, int[][] edges) {
init(n);
for (int[] edge : edges) {
add(edge[0], edge[1], edge[2]);
}
dijkstra(n);
return countLimitPath(1, n);
}
/**
* 给数组初始化分配内存空间
*/
private void init(int n) {
graph = new HashMap<>();
distance = new int[n + 1];
visit = new boolean[n + 1];
priorityQueue = new PriorityQueue<>(Comparator.comparingInt(Edge::getW));
memory = new long[n + 1];
Arrays.fill(memory, -1);
}
/**
* 向graph添加边建图
*/
private void add(int u, int v, int w) {
graph.computeIfAbsent(u, k -> new ArrayList<>()).add(new Edge(u, v, w));
graph.computeIfAbsent(v, k -> new ArrayList<>()).add(new Edge(v, u, w));
}
/**
* 堆优化的Dijkstra算法
*/
private void dijkstra(int n) {
Arrays.fill(distance, 0X3F3F3F3F);
Arrays.fill(visit, false);
distance[n] = 0;
Edge edge;
int node;
priorityQueue.offer(new Edge(n, n, distance[n]));
while (!priorityQueue.isEmpty()) {
// edge 是 未计算最短路结点集合 到 已计算最短路结点集合 最短的边
edge = priorityQueue.poll();
// node 是 未计算最短路结点集合 到 已计算最短路结点集合 最近的结点
// edge.getU()是已计算最短路的结点,所以下面不会出现edge.getU()
node = edge.getV();
if (!visit[node]) {
// 因为node是从优先队列里取出来的,所以node一定满足最近,标记node为已计算最短路结点
visit[node] = true;
// 因为node被标记为已计算最短路结点,更新 已计算最短路结点集合 到 未计算最短路结点集合 的 距离,遍历node的邻接点,看看node结点加入是否造成distance变得更小
for (Edge e : graph.get(node)) {
// e.getV()结点 是 未计算最短路的结点 && n->node + node->v < n->v,更新distance[v]
if (!visit[e.getV()] && distance[node] + e.getW() < distance[e.getV()]) {
distance[e.getV()] = distance[node] + e.getW();
// 把这条边加到优先队列,后序继续找 未计算最短路结点集合 到 已计算最短路结点集合 最短的边
priorityQueue.offer(new Edge(node, e.getV(), distance[e.getV()]));
}
}
}
}
}
/**
* 计算start->end的受限路径,并把结果记忆化,方便后续搜索
* 受限路径的定义:结点i到n的最短路>结点i邻接点j到n的最短路,即distance[i]>distance[j](j是i的邻接点)
* 假设i有两个邻接点j,k,distance[i]>distance[j],distance[i]>distance[k],那么(i,j)和(i,k)都是受限路径
* 则i到n的受限路径数量:limit(i, n) = limit(j, n) + limit(k, n)
*/
private int countLimitPath(int start, int end) {
long result = 0;
if (memory[start] != -1) {
return (int) (memory[start] % MOD);
}
// n的前一个顶点i到n一定是受限路径,因为distance[i] > distance[n] = 0,前一个顶点不好表示,所以把这个1算到n上
if (start == end) {
return 1;
}
for (Edge edge : graph.get(start)) {
if (distance[start] > distance[edge.getV()]) {
result += countLimitPath(edge.getV(), end);
}
}
// 把结果保存起来
memory[start] = result % MOD;
return (int) (result % MOD);
}
/**
* 拓扑排序计算start->end的受限路径
*/
private int countLimitPath(int start, int end) {
int[] limit = new int[end + 1];
limit[end] = 1;
int[][] help = new int[end][2];// 存储结点和结点的distance
for (int i = 0; i < end; i++) {
help[i][0] = i + 1;
help[i][1] = distance[i + 1];
}
// 按照distance升序排列(这里实际是一个拓扑排序),可以保证,要求的limit[i]在之前的过程中已经求出过,直接拿过来就能用
Arrays.sort(help, Comparator.comparingInt(o -> o[1]));
for (int[] peek : help) {
int node = peek[0];
// 访问node的邻接点
for (Edge edge : graph.get(node)) {
// distance[node的邻接点] < distance[node],这条路径是受限路径
if (distance[edge.getV()] < distance[node]) {
// node的受限路径条数 = sum(node邻接点的受限路径数)
limit[node] = (limit[node] + limit[edge.getV()]) % MOD;
}
}
}
return limit[start];
}
}
class Edge {
// u其实用不到,刷题也不必private,自己给自己找麻烦了233
private int u, v, w;
public int getU() {
return u;
}
public void setU(int u) {
this.u = u;
}
public int getV() {
return v;
}
public void setV(int v) {
this.v = v;
}
public int getW() {
return w;
}
public void setW(int w) {
this.w = w;
}
public Edge(int u, int v, int w) {
this.u = u;
this.v = v;
this.w = w;
}
}
作为扩展,再说下使用数组模拟邻接表的做法,这种做法专业名称叫链式前向星。
参考链接
class Solution {
static int[] u;// 第i条边的起始结点(其实用不到)
static int[] v;// 第i条边的结束结点
static int[] w;// 第i条边的权值
static int[] next;// 相同起始结点的下一条边的索引
static int[] head;// 头结点集合
static int index;// 当前遍历到第几条边
public static void main(String[] args) {
int[][] maps = new int[][]{
{1, 2, 1},
{2, 3, 2},
{3, 4, 3},
{1, 3, 4},
{4, 1, 5},
{1, 5, 6},
{4, 5, 7}
};
init(5, 7);
for (int[] map : maps) {
add(map[0], map[1], map[2]);
}
// 对于1 2 1这条边:edge[0].to = 2; edge[0].next = -1; head[1] = 0;
// 对于2 3 2这条边:edge[1].to = 3; edge[1].next = -1; head[2] = 1;
// 对于3 4 3这条边:edge[2].to = 4; edge[2],next = -1; head[3] = 2;
// 对于1 3 4这条边:edge[3].to = 3; edge[3].next = 0; head[1] = 3;
// 对于4 1 5这条边:edge[4].to = 1; edge[4].next = -1; head[4] = 4;
// 对于1 5 6这条边:edge[5].to = 5; edge[5].next = 3; head[1] = 5;
// 对于4 5 7这条边:edge[6].to = 5; edge[6].next = 4; head[4] = 6;
print(5);
}
/**
* n个结点,m条边
*/
private static void init(int n, int m) {
u = new int[m];
v = new int[m];
w = new int[m];
next = new int[m];
head = new int[n + 1];
Arrays.fill(head, -1);
index = 0;
}
private static void add(int _u, int _v, int _w) {
u[index] = _u;
v[index] = _v;
w[index] = _w;
next[index] = head[_u];// 以_u为起点上一条边的编号,也就是与这个边起点相同的上一条边的编号
head[_u] = index++;// 更新以_u为起点上一条边的编号
}
private static void print(int n) {
for (int i = 1; i <= n; i++) {
System.out.println("起始结点" + i);
for (int j = head[i]; j != -1; j = next[j]) {
System.out.println(u[j] + " " + v[j] + " " + w[j]);
}
}
}
}
另外,补上求解最短路的其他算法:Floyd算法、Bellman-Ford算法、SPFA算法。(提一句,这个题后来加数据了,只有优先队列优化的Dijkstra能过,复杂度 O ( M l o g 2 N ) O(Mlog_{2}N) O(Mlog2N)。
private void floyd() {
for (int k = 1; k <= n; k++) {
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= n; j++) {
if (graph[i][j] > graph[i][k] + graph[k][j]) {
graph[i][j] = graph[i][k] + graph[k][j];
}
}
}
}
}
/**
* 返回true表示不存在负环,最短路求解成功
* 返回false表示存在负环,最短路求解无意义
*/
private static boolean bellmanFord(int n) {
Arrays.fill(distance, 0X3F3F3F3F);
distance[n] = 0;
// 做N-1轮松弛
for (int i = 1; i < N; i++) {
// 遍历所有的边,进行松弛
boolean relaxation = false;// 判断是否有松弛操作,某一轮没有松弛操作,即可退出循环
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
int uv = edge[2];
int nu = distance[u];
int nv = distance[v];
if (nu + uv < nv) {
distance[v] = nu + uv;
relaxation = true;
}
// 如果是无向图,还要考虑反向
u = edge[1];
v = edge[0];
uv = edge[2];
nu = distance[u];
nv = distance[v];
if (nu + uv < nv) {
distance[v] = nu + uv;
relaxation = true;
}
}
if (!relaxation) {
break;
}
}
// 经过n-1次松弛后,还有nu + uv < nv的情况,说明存在负环
for (int[] edge : edges) {
int u = edge[0];
int v = edge[1];
int uv = edge[2];
int nu = distance[u];
int nv = distance[v];
if (nu + uv < nv) {
return false;
}
}
return true;
}
/**
* 返回false,表示存在负环,最短路无意义
* 返回true,表示成功求得最短路,最短路在distance数组中
*/
private boolean spfa(int n) {
Arrays.fill(inQueue, false);
Arrays.fill(count, 0);
Arrays.fill(distance, 0X3F3F3F3F);
queue = new LinkedList<>();
distance[n] = 0;
queue.offer(new Edge(n, n, distance[n]));
inQueue[n] = true;
count[n]++;
Edge edge;
int node;
while (!queue.isEmpty()) {
edge = queue.poll();// edge 是 未计算最短路结点集合 到 已计算最短路结点集合 的 任意一条边
node = edge.getV();// node 是 edge的一个结点
inQueue[node] = false;// 标记node出队列
for (Edge e : graph.get(node)) {// 遍历node的邻接点,判断是否需要松弛
// n->node + node->v < n->v,进行松弛
if (distance[node] + e.getW() < distance[e.getV()]) {
distance[e.getV()] = distance[node] + e.getW();
// spfa允许结点多次进入队列
if (!inQueue[e.getV()]) {
queue.offer(new Edge(node, e.getV(), distance[e.getV()]));
// 标记入队
inQueue[e.getV()] = true;
// 统计入队次数
count[e.getV()]++;
// 入队次数大于结点总数,说明存在负环
if (count[e.getV()] > N) {
return false;
}
}
}
}
}
return true;
}
第四题:使所有区间的异或结果为零
把题解写到代码里了,实在是太难了,看着别人的题解想了好久,大概能看明白了,自己写,哎,还是写不出来。
class Solution {
/**
* 分析前两个长度为k的区间
* 区间a:nums[0] ~ nums[k - 1]这k个数字,异或结果是0
* 区间b:nums[1] ~ nums[k]这k个数字,异或结果是0
* 区间a和区间b公共部分是nums[1] ~ nums[k - 1],区间a的异或 = 0 = 区间b的异或,可以得到结论:nums[0] = nums[k]
* 假设,将nums放到一个二维数组中,二维数组的列数col = k,二维数组的行数row = (length + k - 1) / k
* 同理,可以得到nums的最后状态如下
* 第0列:nums[0 * k] = nums[1 * k] = nums[2 * k] = …… = nums[(row - 1) * k]
* 第1列:nums[0 * k + 1] = nums[1 * k + 1] = nums[2 * k + 1] = …… = nums[(row - 1) * k + 1]
* ……
* 第k - 1列:nums[0 * k + k -1] = nums[1 * k + k -1] = nums[2 * k + k -1] = …… = nums[(row - 1) * k + k -1]
* 当然,最后一行可能不足k个,不过不影响
* 把每一列看做一组,最后的结果是第1组所有数字都变成a,第2组所有数字都变成b,……,第k - 1组所有数字都变成x
* 使得a ^ b ^ …… ^ x = 0
* 因为同一组里最终都变成同一个数,我们只需要考虑[0, k - 1]这个区间即可
* nums[i]变成什么,就是我们要分析的,假设nums[i]最终变成了goal
* 这个goal就有两种情况,它们对应的代价是不同的
* 用cost表示第i列数字都改成goal需要的代价,用frequency[i].get(goal)表示原来第i列中,goal数字出现的次数,用size[i]表示第i列数字总数量
* cost = size[i] - frequency[i].get(goal)
* 1.goal不来自第i列的某个值:frequency[i].get(goal) = 0, cost = size[i] - frequency[i].get(goal) = size[i]
* 2.goal来自第i列的某个值:cost = size[i] - frequency[i].get(goal)
* 介绍dp数组,dp[i - 1][j]的结果代表:nums[0] ^ nums[1] ^ …… ^ nums[i - 1] = j所需要的最小代价
* 我们要把nums[i]变成goal,那么nums[0] ^ nums[1] ^ …… ^ nums[i - 1] ^ nums[i] = j ^ goal,即dp[i][j ^ goal]
* 从而,状态转移方程:dp[i][j ^ goal] = dp[i - 1][j] + cost[goal]
* 这里一定要分清楚,第i列的目标值goal,前i列的异或值j ^ goal,这两者的区别,一开始理解的时候混成一个意思了
* 然后分析这个goal的情况,goal的情况不同,cost[goal]就不同
* 最后dp[k][0]就是答案
* 观察数据范围,nums[i]∈[0,1023],任何两个nums[i]异或的结果也不会超过这个范围
*/
public int minChanges(int[] nums, int k) {
int length = nums.length, MAX = 1024, INF = 0X3F3F3F3F, cost;
int[] size = new int[k];
int[][] dp = new int[k + 1][MAX];
// HashMap数组,frequency[i]表示第i组的情况,key是数字,value是这个数字的频次
HashMap<Integer, Integer>[] frequency = new HashMap[k];
// 统计第i组的数字总数量,统计第i列中,number出现的频次
for (int i = 0; i < length; i++) {
int index = i % k;
int number = nums[i];
size[index]++;
if (frequency[index] == null) {
frequency[index] = new HashMap<>();
}
// 先get(number),如果是null,put(number, 1),否则让value + 1
frequency[index].merge(number, 1, Integer::sum);
}
// dp数组初始化
for (int i = 0; i <= k; i++) {
for (int j = 0; j < MAX; j++) {
dp[i][j] = INF;
}
}
dp[0][0] = 0;
for (int i = 1; i <= k; i++) {
// 第i列变成的goal不来自原来的第i - 1列,cost = size[i - 1],意味着cost只和i - 1列数字个数有关
cost = size[i - 1];
// 要dp[i][j]想最小,就要取dp[i - 1][j]的最小值 + cost
int min = INF;
// 找dp[i - 1][j]的最小值(其实这里包含goal来自第i - 1列数字的情况,但是这里cost较大,还会被下面的更小的cost情况更新掉)
for (int j = 0; j < MAX; j++) {
min = Math.min(min, dp[i - 1][j]);
}
// 更新dp数组
for (int j = 0; j < MAX; j++) {
dp[i][j] = min + cost;
}
// 第i - 1列变成的goal来自原来的第i - 1列,cost = size[i- 1] - frequency[i - 1].get(goal)
// 遍历第i - 1列所有的数字
for (int number : frequency[i - 1].keySet()) {
for (int j = 0; j < MAX; j++) {
cost = size[i - 1] - frequency[i - 1].get(number);
dp[i][j ^ number] = Math.min(dp[i][j ^ number], dp[i - 1][j] + cost);
}
}
}
return dp[k][0];
}
}