#P4448. 第3题-卷积操作
-
1000ms
Tried: 3
Accepted: 2
Difficulty: 5
所属公司 :
华为
时间 :2025年11月6日-留学生AI方向
-
算法标签>模拟
第3题-卷积操作
解题思路
多通道卷积是卷积神经网络(CNN)中的核心运算。其本质是:在每个滑动位置,将输入在所有通道上的同形窗口与对应通道的卷积核做元素乘加(逐通道累加),得到一个标量作为该位置的输出值。
-
填充(padding) 按题意在输入张量四周补零,得到形状为 (C,Hin+2p,Win+2p) 的新张量(p=padding)。
-
滑动窗口(stride) 以步长 s(s=stride)在高、宽方向移动卷积核窗口。仅当窗口完全落在填充后的输入内时才计算。输出尺寸为
$$H_{out}=\left\lfloor\frac{H_{in}+2p-K_h}{s}\right\rfloor+1,\quad W_{out}=\left\lfloor\frac{W_{in}+2p-K_w}{s}\right\rfloor+1 $$ -
逐通道累加(核心计算) 在位置 (i,j) 处的输出值:
$$\text{out}(i,j)=\sum_{c=0}^{C-1}\sum_{m=0}^{K_h-1}\sum_{n=0}^{K_w-1} X_c(i\cdot s+m,\ j\cdot s+n)\cdot K_c(m,n) $$其中 X 为填充后的输入,K 为卷积核。中间计算用整数即可(题面输出也为整数)。
-
实现方法
- 读入参数与数据,构造三维输入与核。
- 先做零填充,得到新三维数组。
- 依据上式三重(或四重)循环完成滑动与乘加。
- 输出 Hout×Wout 的二维矩阵。
本题无需使用快速傅里叶等高级算法,直接按定义实现即可,参数规模很小(C≤6,Hin,Win<10)。
复杂度分析
-
时间复杂度: 对每个输出位置做 C×Kh×Kw 次乘加,输出一共 Hout×Wout 个位置,
$$O\big(H_{out}\cdot W_{out}\cdot C\cdot K_h\cdot K_w\big) $$在题目约束下规模很小,完全可行。
-
空间复杂度: 需要存储填充后的输入 O(C⋅(Hin+2p)⋅(Win+2p)),卷积核 O(C⋅Kh⋅Kw),以及输出 O(Hout⋅Wout)。总体为
$$O\big(C(H_{in}+2p)(W_{in}+2p)+C K_h K_w+H_{out}W_{out}\big) $$同样很小,合理。
代码实现
Python
import sys
# 多通道卷积:输入input_tensor和kernel均为三维列表
def conv_multi_channel(input_tensor, kernel, stride, padding):
C = len(input_tensor)
Hin = len(input_tensor[0])
Win = len(input_tensor[0][0])
Kh = len(kernel[0])
Kw = len(kernel[0][0])
# 计算输出尺寸
Hout = (Hin + 2 * padding - Kh) // stride + 1
Wout = (Win + 2 * padding - Kw) // stride + 1
# 1) 先做零填充
Hp = Hin + 2 * padding
Wp = Win + 2 * padding
padded = [[[0 for _ in range(Wp)] for _ in range(Hp)] for _ in range(C)]
for c in range(C):
for i in range(Hin):
for j in range(Win):
padded[c][i + padding][j + padding] = input_tensor[c][i][j]
# 2) 按定义滑动窗口并逐通道累加
output = [[0 for _ in range(Wout)] for _ in range(Hout)]
for i in range(Hout):
for j in range(Wout):
s = 0
base_i = i * stride
base_j = j * stride
for c in range(C):
for m in range(Kh):
for n in range(Kw):
s += padded[c][base_i + m][base_j + n] * kernel[c][m][n]
output[i][j] = s
return output
def main():
data = []
for line in sys.stdin:
line = line.strip()
if line:
data.extend(map(int, line.split()))
ptr = 0
# 读输入张量
C, Hin, Win = data[ptr], data[ptr+1], data[ptr+2]; ptr += 3
input_tensor = []
for _ in range(C):
ch = []
for _ in range(Hin):
row = data[ptr:ptr+Win]; ptr += Win
ch.append(row)
input_tensor.append(ch)
# 读卷积核
Ck, Kh, Kw = data[ptr], data[ptr+1], data[ptr+2]; ptr += 3
# 题目保证 Ck == C
kernel = []
for _ in range(Ck):
ch = []
for _ in range(Kh):
row = data[ptr:ptr+Kw]; ptr += Kw
ch.append(row)
kernel.append(ch)
# 读 stride 和 padding
stride, padding = data[ptr], data[ptr+1]
# 计算卷积
out = conv_multi_channel(input_tensor, kernel, stride, padding)
# 输出
for i in range(len(out)):
print(" ".join(str(x) for x in out[i]))
if __name__ == "__main__":
main()
Java
import java.util.*;
public class Main {
// 多通道卷积:inputTensor、kernel均为三维数组 [C][H][W]
static int[][] convMultiChannel(int[][][] inputTensor, int[][][] kernel, int stride, int padding) {
int C = inputTensor.length;
int Hin = inputTensor[0].length;
int Win = inputTensor[0][0].length;
int Kh = kernel[0].length;
int Kw = kernel[0][0].length;
int Hout = (Hin + 2 * padding - Kh) / stride + 1;
int Wout = (Win + 2 * padding - Kw) / stride + 1;
// 1) 零填充
int Hp = Hin + 2 * padding, Wp = Win + 2 * padding;
int[][][] padded = new int[C][Hp][Wp];
for (int c = 0; c < C; c++) {
for (int i = 0; i < Hin; i++) {
for (int j = 0; j < Win; j++) {
padded[c][i + padding][j + padding] = inputTensor[c][i][j];
}
}
}
// 2) 滑动窗口并逐通道累加
int[][] output = new int[Hout][Wout];
for (int i = 0; i < Hout; i++) {
for (int j = 0; j < Wout; j++) {
int sum = 0;
int baseI = i * stride;
int baseJ = j * stride;
for (int c = 0; c < C; c++) {
for (int m = 0; m < Kh; m++) {
for (int n = 0; n < Kw; n++) {
sum += padded[c][baseI + m][baseJ + n] * kernel[c][m][n];
}
}
}
output[i][j] = sum;
}
}
return output;
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读入输入张量
int C = sc.nextInt(), Hin = sc.nextInt(), Win = sc.nextInt();
int[][][] inputTensor = new int[C][Hin][Win];
for (int c = 0; c < C; c++) {
for (int i = 0; i < Hin; i++) {
for (int j = 0; j < Win; j++) {
inputTensor[c][i][j] = sc.nextInt();
}
}
}
// 读入卷积核
int Ck = sc.nextInt(), Kh = sc.nextInt(), Kw = sc.nextInt();
int[][][] kernel = new int[Ck][Kh][Kw];
for (int c = 0; c < Ck; c++) {
for (int i = 0; i < Kh; i++) {
for (int j = 0; j < Kw; j++) {
kernel[c][i][j] = sc.nextInt();
}
}
}
// stride 和 padding
int stride = sc.nextInt(), padding = sc.nextInt();
int[][] out = convMultiChannel(inputTensor, kernel, stride, padding);
// 输出
StringBuilder sb = new StringBuilder();
for (int i = 0; i < out.length; i++) {
for (int j = 0; j < out[0].length; j++) {
if (j > 0) sb.append(' ');
sb.append(out[i][j]);
}
sb.append('\n');
}
System.out.print(sb.toString());
sc.close();
}
}
C++
#include <bits/stdc++.h>
using namespace std;
// 多通道卷积:输入和核均为三维 vector [C][H][W]
vector<vector<int>> conv_multi_channel(const vector<vector<vector<int>>>& input_tensor,
const vector<vector<vector<int>>>& kernel,
int stride, int padding) {
int C = (int)input_tensor.size();
int Hin = (int)input_tensor[0].size();
int Win = (int)input_tensor[0][0].size();
int Kh = (int)kernel[0].size();
int Kw = (int)kernel[0][0].size();
int Hout = (Hin + 2 * padding - Kh) / stride + 1;
int Wout = (Win + 2 * padding - Kw) / stride + 1;
// 1) 零填充
int Hp = Hin + 2 * padding, Wp = Win + 2 * padding;
vector<vector<vector<int>>> padded(C, vector<vector<int>>(Hp, vector<int>(Wp, 0)));
for (int c = 0; c < C; ++c) {
for (int i = 0; i < Hin; ++i) {
for (int j = 0; j < Win; ++j) {
padded[c][i + padding][j + padding] = input_tensor[c][i][j];
}
}
}
// 2) 滑动窗口并逐通道累加
vector<vector<int>> output(Hout, vector<int>(Wout, 0));
for (int i = 0; i < Hout; ++i) {
for (int j = 0; j < Wout; ++j) {
int sum = 0;
int base_i = i * stride;
int base_j = j * stride;
for (int c = 0; c < C; ++c) {
for (int m = 0; m < Kh; ++m) {
for (int n = 0; n < Kw; ++n) {
sum += padded[c][base_i + m][base_j + n] * kernel[c][m][n];
}
}
}
output[i][j] = sum;
}
}
return output;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 读入输入张量
int C, Hin, Win;
if (!(cin >> C >> Hin >> Win)) return 0;
vector<vector<vector<int>>> input_tensor(C, vector<vector<int>>(Hin, vector<int>(Win)));
for (int c = 0; c < C; ++c) {
for (int i = 0; i < Hin; ++i) {
for (int j = 0; j < Win; ++j) {
cin >> input_tensor[c][i][j];
}
}
}
// 读入卷积核
int Ck, Kh, Kw;
cin >> Ck >> Kh >> Kw;
vector<vector<vector<int>>> kernel(Ck, vector<vector<int>>(Kh, vector<int>(Kw)));
for (int c = 0; c < Ck; ++c) {
for (int i = 0; i < Kh; ++i) {
for (int j = 0; j < Kw; ++j) {
cin >> kernel[c][i][j];
}
}
}
int stride, padding;
cin >> stride >> padding;
// 计算卷积
auto out = conv_multi_channel(input_tensor, kernel, stride, padding);
// 输出
for (int i = 0; i < (int)out.size(); ++i) {
for (int j = 0; j < (int)out[0].size(); ++j) {
if (j) cout << ' ';
cout << out[i][j];
}
cout << '\n';
}
return 0;
}
题目内容
卷积神经网络常用于图像分类、检测与分割等图像任务。多通道卷积的计算公式是卷积神经网络 (CNN) 中的核心运算,请用代码实现多通道卷积操作。
输入: input: 形状为 (C,H_in,W_in) 的输入张量,C 为输入通道数,H_in 为输入高度,W_in 为输入宽度
kernel: 形状为 (C,K_h,K_w) 的卷积核,C 为输入通道数(和 input 的 C 数值相同),K_h 为输入高度,K_w 为输入宽度
stride:卷积步长(大于等于 1 的整数)
其中
0<C<6
2<H_in<10
2<W_in<10
0<K_h<7
0<K_w<7
0<stride<4
0=<padding=<3
卷积操作过程说明:
1.填充:在输入张量的上下左右各填充 “0” ,填充后的形状为 (C,H_in+2padding,W_in+2padding)。
2 滑动窗口计算:从填充后的输入张量的左上角开始,按照 stride 步长滑动卷积核,每次取与卷积核形状相同的子区域。若剩余区域尺寸小于卷积核尺寸,则跳过该位置。
3,逐通道计算:对于每个子区域,将卷积核与对应位置的输入值逐通道相乘后求和(中间计算过程不做四舍五入),得到输出张量的一个值,计算公式为:$\operatorname{output}(i, j)=\sum_{c=0}^{C-1} \sum_{m=0}^{K_{b}-1} \sum_{n=0}^{K_{n}-1} \text { input }_{c}(i \times \text { stride }+m, j \times \text { stride }+n) \times \operatorname{kernel}_{c}(m, n)$
4.输出结果:将所有子区域的计算结果组合成 (H_out,W_out) 的输出张量。
输出:
形状为 (H_out,W_out) 的 2D 数组,其中:
H_out=(H_in+2∗padding−k)//stride+1
W_out=(W_in+2∗padding−K)//stride+1
注: // 代表除法后向下取整

上图为单通道卷积 padding 为 1,stride 为 1 时示意图,多通道卷积时还需对各通道进行求和,题中不再绘制示意图
输入描述
第一行 C,H_in,W_in 表示输入的张量的通道数、行数与列数;
接下来的 C×H_in 行是此张量的元素值;
接下来一行 C,K_h,K_w 是卷积核的通道数、行数与列数;
接下来 C×H_in是卷积核的元素值;
最后一行是 stride padding
输出描述
输出卷积后形状为 (H_out,W_out) 的特征图(二维矩阵),元素均为整数。
样例1
输入
2 3 3
1 2 3
4 5 6
7 8 9
2 3 4
5 6 7
8 9 10
2 2 2
1 0
0 1
2 0
0 2
1 0
输出
22 28
40 46
说明
第一行 2 3 3 表示输入的张量为 2 通道,每个通道的行数和列数均为 3
第 2−4 行是此张量第一通道的元素值
第 5−7 行是此张量第二通道的元素值
第 8 行的 2 2 2 是卷积核的形状,其通道数与输入张量一致,都是 2 ,行数和列数都是 2
第 9、10 行是此卷积核第 1 通道的元素值
第 11、12 行是此卷积核第 2 通道的元素值
最后一行 2 1 代表卷积操作中卷积窗滑动的步长 stride 为 1 ,padding 为 0
-
计算过程:
-
输出尺寸: H_out=13+2×0−2+1=2,W_out=2
-
输出矩阵位置 (0,0) 的计算过程如下:
通道 1 :窗口 [[1,2],[4,5]]→(1×1)+(2×0)+(4×0)+(5×1)=6
通道 2 :窗口 [[2,3],[5,6]]→(2×2)+(3×0)+(5×0)+(6×2)=4+12=16
各通道求和: 6+16=22
-
输出矩阵位置 (0,1) 和 (1,0) 元素计算过程略。
-
位置 (1,1) 计算:
通道 1 :窗口 [[5,6],[8,9]]→(5×1)+(6×0)+(8×0)+(9×1)=14
通道 2 :窗口 [[6,71,[9,10]]→(6×2)+(7×0)+(9×0)+(10×2)=12+20=32
各通道总和: 14+32=46